diff --git a/ui/angular.json b/ui/angular.json
index bf416b4bd..c4745d462 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -29,6 +29,11 @@
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
+ },
+ {
+ "glob": "**/*.svg",
+ "input": "src/assets/icon",
+ "output": "./svg"
}
],
"styles": [
diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts
index 55cad94dd..6f2e8db05 100644
--- a/ui/src/app/app-routing.module.ts
+++ b/ui/src/app/app-routing.module.ts
@@ -33,6 +33,11 @@ const routes: Routes = [
canActivateChild: [AuthGuard],
loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule),
},
+ {
+ path: 'drives',
+ canActivate: [AuthGuard],
+ loadChildren: () => import('./pages/server-routes/external-drives/external-drives.module').then( m => m.ExternalDrivesPageModule),
+ },
]
@NgModule({
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 233c8374c..b6afe40cc 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -47,6 +47,11 @@ export class AppComponent {
url: '/notifications',
icon: 'notifications-outline',
},
+ {
+ title: 'External Drives',
+ url: '/drives',
+ icon: 'albums-outline',
+ },
]
constructor (
diff --git a/ui/src/app/models/server-model.ts b/ui/src/app/models/server-model.ts
index 377389169..bcee0c2cf 100644
--- a/ui/src/app/models/server-model.ts
+++ b/ui/src/app/models/server-model.ts
@@ -161,7 +161,7 @@ export interface DiskInfo {
export interface DiskPartition {
logicalname: string,
- isMounted: boolean, // Do not let them back up to this if true
+ isMounted: boolean, // We do not allow backups to mounted partitions
size: string | null,
label: string | null,
}
diff --git a/ui/src/app/pages/server-routes/external-drives/external-drives.module.ts b/ui/src/app/pages/server-routes/external-drives/external-drives.module.ts
new file mode 100644
index 000000000..066b57a3a
--- /dev/null
+++ b/ui/src/app/pages/server-routes/external-drives/external-drives.module.ts
@@ -0,0 +1,30 @@
+import { NgModule } from '@angular/core'
+import { CommonModule } from '@angular/common'
+import { IonicModule } from '@ionic/angular'
+import { ExternalDrivesPage } from './external-drives.page'
+import { Routes, RouterModule } from '@angular/router'
+import { SharingModule } from 'src/app/modules/sharing.module'
+import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
+import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
+import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ExternalDrivesPage,
+ },
+]
+
+@NgModule({
+ imports: [
+ CommonModule,
+ IonicModule,
+ SharingModule,
+ ObjectConfigComponentModule,
+ RouterModule.forChild(routes),
+ PwaBackComponentModule,
+ BadgeMenuComponentModule,
+ ],
+ declarations: [ExternalDrivesPage],
+})
+export class ExternalDrivesPageModule { }
diff --git a/ui/src/app/pages/server-routes/external-drives/external-drives.page.html b/ui/src/app/pages/server-routes/external-drives/external-drives.page.html
new file mode 100644
index 000000000..332d8f531
--- /dev/null
+++ b/ui/src/app/pages/server-routes/external-drives/external-drives.page.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+ External Drives
+
+
+
+
+
+
+
+
+
+
+
+
+ Storage
+
+
+
+
+
+
+ {{d.logicalname}}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/app/pages/server-routes/external-drives/external-drives.page.scss b/ui/src/app/pages/server-routes/external-drives/external-drives.page.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/src/app/pages/server-routes/external-drives/external-drives.page.ts b/ui/src/app/pages/server-routes/external-drives/external-drives.page.ts
new file mode 100644
index 000000000..88890b1b0
--- /dev/null
+++ b/ui/src/app/pages/server-routes/external-drives/external-drives.page.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core'
+import { pauseFor } from 'src/app/util/misc.util'
+import { ApiService } from 'src/app/services/api/api.service'
+import { DiskInfo } from 'src/app/models/server-model'
+import { markAsLoadingDuring$, markAsLoadingDuringAsync, markAsLoadingDuringP } from 'src/app/services/loader.service'
+import { BehaviorSubject } from 'rxjs'
+import { AlertController } from '@ionic/angular'
+
+type Ejectable = T & { $ejecting$: BehaviorSubject }
+
+@Component({
+ selector: 'external-drives',
+ templateUrl: './external-drives.page.html',
+ styleUrls: ['./external-drives.page.scss'],
+})
+export class ExternalDrivesPage {
+ disks: Ejectable[] = []
+ $loading$ = new BehaviorSubject(false)
+
+ constructor (
+ private readonly apiService: ApiService,
+ private readonly alertCtrl: AlertController,
+ ) { }
+
+ ngOnInit () {
+ markAsLoadingDuringP(this.$loading$, this.fetchDisks())
+ }
+
+ async doRefresh (event: any) {
+ await Promise.all([
+ this.fetchDisks(),
+ pauseFor(600),
+ ])
+ event.target.complete()
+ }
+
+ async fetchDisks () {
+ return this.apiService.getExternalDisks().then(ds => {
+ this.disks = ds.map(d => ({ ...d, $ejecting$: new BehaviorSubject(false)})).sort( (a, b) => a.logicalname < b.logicalname ? -1 : 1 )
+ })
+ }
+
+ async ejectDisk (diskIndex: number) {
+ const d = this.disks[diskIndex]
+ markAsLoadingDuringP(d.$ejecting$, this.apiService.ejectExternalDisk(d.logicalname))
+ .then(() => this.disks.splice(diskIndex, 1))
+ .catch((e: Error) => {
+ this.alertError(`Could not eject ${d.logicalname}: ${e.message}`)
+ })
+ }
+
+ async alertError (desc: string) {
+ const alert = await this.alertCtrl.create({
+ backdropDismiss: true,
+ message: desc,
+ cssClass: 'alert-error-message',
+ })
+ await alert.present()
+ }
+}
+
diff --git a/ui/src/app/pages/server-routes/server-routing.module.ts b/ui/src/app/pages/server-routes/server-routing.module.ts
index c4cd29ec2..78f5516fc 100644
--- a/ui/src/app/pages/server-routes/server-routing.module.ts
+++ b/ui/src/app/pages/server-routes/server-routing.module.ts
@@ -37,7 +37,7 @@ const routes: Routes = [
path: 'developer',
canActivate: [AuthGuard],
loadChildren: () => import('./developer-routes/developer-routing.module').then( m => m.DeveloperRoutingModule),
- },
+ }
]
@NgModule({
diff --git a/ui/src/app/services/api/api.service.ts b/ui/src/app/services/api/api.service.ts
index 7678b3ba6..cc73fa08c 100644
--- a/ui/src/app/services/api/api.service.ts
+++ b/ui/src/app/services/api/api.service.ts
@@ -57,6 +57,7 @@ export abstract class ApiService {
abstract deleteWifi (ssid: string): Promise
abstract restartServer (): Promise
abstract shutdownServer (): Promise
+ abstract ejectExternalDisk (logicalName: string): Promise
}
export module ReqRes {
diff --git a/ui/src/app/services/api/live-api.service.ts b/ui/src/app/services/api/live-api.service.ts
index 1e54ee770..31c6da4a6 100644
--- a/ui/src/app/services/api/live-api.service.ts
+++ b/ui/src/app/services/api/live-api.service.ts
@@ -9,7 +9,7 @@ import { HttpErrorResponse } from '@angular/common/http'
import { isUnauthorized } from 'src/app/util/web.util'
import { Replace } from 'src/app/util/types.util'
import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util'
-import { modulateTime } from 'src/app/util/misc.util'
+// import { modulateTime } from 'src/app/util/misc.util'
@Injectable()
export class LiveApiService extends ApiService {
@@ -61,6 +61,10 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.GET, url: `/disks` })
}
+ async ejectExternalDisk (logicalName: string): Promise {
+ return this.authRequest({ method: Method.DELETE, url: `/disks/${logicalName}` })
+ }
+
async updateAgent (version: string): Promise {
const data: ReqRes.PostUpdateAgentReq = {
version: `=${version}`,
diff --git a/ui/src/app/services/api/mock-api.service.ts b/ui/src/app/services/api/mock-api.service.ts
index ff109010c..ba5308a33 100644
--- a/ui/src/app/services/api/mock-api.service.ts
+++ b/ui/src/app/services/api/mock-api.service.ts
@@ -29,13 +29,17 @@ export class MockApiService extends ApiService {
async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
await pauseFor(2000)
throw new Error ('some misc backend error ohh we forgot to make this endpoint or something')
- // return { config: mockCupsDependentConfig, breakages: [ ] }
}
async getServer (): Promise {
return mockGetServer()
}
+ async ejectExternalDisk (): Promise {
+ await pauseFor(2000)
+ return { }
+ }
+
async getCheckAuth (): Promise {
return { }
}
diff --git a/ui/src/assets/icon/eject-outline.svg b/ui/src/assets/icon/eject-outline.svg
new file mode 100644
index 000000000..6ecd2a10b
--- /dev/null
+++ b/ui/src/assets/icon/eject-outline.svg
@@ -0,0 +1,4 @@
+
diff --git a/ui/src/assets/icon/eject.svg b/ui/src/assets/icon/eject.svg
new file mode 100644
index 000000000..3e2a87bc1
--- /dev/null
+++ b/ui/src/assets/icon/eject.svg
@@ -0,0 +1,4 @@
+
diff --git a/ui/src/global.scss b/ui/src/global.scss
index 68f636155..1b9948100 100644
--- a/ui/src/global.scss
+++ b/ui/src/global.scss
@@ -194,6 +194,12 @@ ion-popover {
}
}
+.alert-error-message {
+ .alert-message {
+ color: var(--ion-color-danger);
+ }
+}
+
ion-slides {
.slider-wrapper {
height: 100%;
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
index b55f72a21..af07fe92d 100644
--- a/ui/tsconfig.json
+++ b/ui/tsconfig.json
@@ -33,6 +33,7 @@
"src/polyfills.ts",
],
"include": [
- "src/**/*.d.ts"
+ "src/**/*.d.ts",
+ "*.d.ts",
]
}