diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index b49c52c05..326e37928 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -2,8 +2,6 @@ import { NgModule } from '@angular/core' import { PreloadAllModules, RouterModule, Routes } from '@angular/router' import { AuthGuard } from './guards/auth.guard' import { UnauthGuard } from './guards/unauth.guard' -import { MaintenanceGuard } from './guards/maintenance.guard' -import { UnmaintenanceGuard } from './guards/unmaintenance.guard' const routes: Routes = [ { @@ -18,30 +16,25 @@ const routes: Routes = [ }, { path: 'embassy', - canActivate: [AuthGuard, MaintenanceGuard], - canActivateChild: [AuthGuard, MaintenanceGuard], + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], loadChildren: () => import('./pages/server-routes/server-routing.module').then(m => m.ServerRoutingModule), }, - { - path: 'maintenance', - canActivate: [AuthGuard, UnmaintenanceGuard], - loadChildren: () => import('./pages/maintenance/maintenance.module').then(m => m.MaintenancePageModule), - }, { path: 'marketplace', - canActivate: [AuthGuard, MaintenanceGuard], - canActivateChild: [AuthGuard, MaintenanceGuard], + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], loadChildren: () => import('./pages/marketplace-routes/marketplace-routing.module').then(m => m.MarketplaceRoutingModule), }, { path: 'notifications', - canActivate: [AuthGuard, MaintenanceGuard], + canActivate: [AuthGuard], loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule), }, { path: 'services', - canActivate: [AuthGuard, MaintenanceGuard], - canActivateChild: [AuthGuard, MaintenanceGuard], + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule), }, ] diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index a0acae810..5f60563e9 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -87,6 +87,7 @@ + diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 338c223bd..3862ef698 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -240,16 +240,6 @@ export class AppComponent { private watchStatus (): Subscription { return this.patch.watch$('server-info', 'status') .subscribe(status => { - const maintenance = '/maintenance' - const route = this.router.url - if (status === ServerStatus.Running && route.startsWith(maintenance)) { - this.showMenu = true - this.router.navigate([''], { replaceUrl: true }) - } - if (status === ServerStatus.BackingUp && !route.startsWith(maintenance)) { - this.showMenu = false - this.router.navigate([maintenance], { replaceUrl: true }) - } if (status === ServerStatus.Updating) { this.watchUpdateProgress() } diff --git a/ui/src/app/components/backup-drives/backup-drives-header.component.html b/ui/src/app/components/backup-drives/backup-drives-header.component.html new file mode 100644 index 000000000..20e6f553b --- /dev/null +++ b/ui/src/app/components/backup-drives/backup-drives-header.component.html @@ -0,0 +1,16 @@ + + + + + + + + {{ title }} + + + Refresh + + + + + \ No newline at end of file diff --git a/ui/src/app/components/backup-drives/backup-drives.component.html b/ui/src/app/components/backup-drives/backup-drives.component.html new file mode 100644 index 000000000..f856a0570 --- /dev/null +++ b/ui/src/app/components/backup-drives/backup-drives.component.html @@ -0,0 +1,70 @@ + + + + {{ message }} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ backupService.loadingError }} + + + + + + + + + + No drives found. Insert a backup drive into your Embassy and click "Refresh" above. + + + + + + +
+ + {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }} - {{ drive.capacity | convertBytes }} + + + + +

{{ partition.label || partition.logicalname }}

+

{{ partition.capacity | convertBytes }}

+

+ + Embassy backups detected + +

+
+ +
+
+
+
+
+
+
diff --git a/ui/src/app/components/backup-drives/backup-drives.component.module.ts b/ui/src/app/components/backup-drives/backup-drives.component.module.ts new file mode 100644 index 000000000..3663ffa25 --- /dev/null +++ b/ui/src/app/components/backup-drives/backup-drives.component.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { BackupDrivesComponent, BackupDrivesHeaderComponent } from './backup-drives.component' +import { SharingModule } from '../../modules/sharing.module' + +@NgModule({ + declarations: [ + BackupDrivesComponent, + BackupDrivesHeaderComponent, + ], + imports: [ + CommonModule, + IonicModule, + SharingModule, + ], + exports: [ + BackupDrivesComponent, + BackupDrivesHeaderComponent, + ], +}) +export class BackupDrivesComponentModule { } diff --git a/ui/src/app/components/backup-drives/backup-drives.component.scss b/ui/src/app/components/backup-drives/backup-drives.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/backup-drives/backup-drives.component.ts b/ui/src/app/components/backup-drives/backup-drives.component.ts new file mode 100644 index 000000000..381c41f5f --- /dev/null +++ b/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -0,0 +1,63 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { BackupService } from './backup.service' +import { MappedPartitionInfo } from 'src/app/util/misc.util' + +@Component({ + selector: 'backup-drives', + templateUrl: './backup-drives.component.html', + styleUrls: ['./backup-drives.component.scss'], +}) +export class BackupDrivesComponent { + @Input() type: 'backup' | 'recover' + @Output() onSelect: EventEmitter = new EventEmitter() + message: string + + constructor ( + public readonly backupService: BackupService, + ) { } + + ngOnInit () { + if (this.type === 'backup') { + this.message = 'Select the drive where you want to create a backup of your Embassy.' + } else { + this.message = 'Select the drive containing the backup you would like to restore.' + } + this.backupService.getExternalDrives() + } + + handleSelect (partition: MappedPartitionInfo): void { + this.onSelect.emit(partition) + } +} + + +@Component({ + selector: 'backup-drives-header', + templateUrl: './backup-drives-header.component.html', + styleUrls: ['./backup-drives.component.scss'], +}) +export class BackupDrivesHeaderComponent { + @Input() type: 'backup' | 'recover' + @Output() onClose: EventEmitter = new EventEmitter() + title: string + + constructor ( + public readonly backupService: BackupService, + ) { } + + ngOnInit () { + if (this.type === 'backup') { + this.title = 'Create Backup' + } else { + this.title = 'Restore From Backup' + } + } + + close (): void { + this.onClose.emit() + } + + refresh () { + this.backupService.getExternalDrives() + } +} diff --git a/ui/src/app/components/backup-drives/backup.service.ts b/ui/src/app/components/backup-drives/backup.service.ts new file mode 100644 index 000000000..d98bf3056 --- /dev/null +++ b/ui/src/app/components/backup-drives/backup.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core' +import { IonicSafeString } from '@ionic/core' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { getErrorMessage } from 'src/app/services/error-toast.service' +import { MappedDriveInfo, MappedPartitionInfo } from 'src/app/util/misc.util' +import { Emver } from 'src/app/services/emver.service' + +@Injectable({ + providedIn: 'root', +}) +export class BackupService { + drives: MappedDriveInfo[] + loading = true + loadingError: string | IonicSafeString + + constructor ( + private readonly embassyApi: ApiService, + private readonly emver: Emver, + ) { } + + async getExternalDrives (): Promise { + this.loading = true + + try { + const drives = await this.embassyApi.getDrives({ }) + this.drives = drives.map(d => { + const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => { + return { + ...p, + hasBackup: [0, 1].includes(this.emver.compare(p['embassy-os']?.version, '0.3.0')), + } + }) + return { + ...d, + partitions: partionInfo, + } + }) + } catch (e) { + this.loadingError = getErrorMessage(e) + } finally { + this.loading = false + } + } + +} \ No newline at end of file diff --git a/ui/src/app/components/skeleton-list/skeleton-list.component.scss b/ui/src/app/components/skeleton-list/skeleton-list.component.scss index 730c11ca5..e69de29bb 100644 --- a/ui/src/app/components/skeleton-list/skeleton-list.component.scss +++ b/ui/src/app/components/skeleton-list/skeleton-list.component.scss @@ -1,3 +0,0 @@ -ion-note { - width: 50%; -} diff --git a/ui/src/app/guards/maintenance.guard.ts b/ui/src/app/guards/maintenance.guard.ts deleted file mode 100644 index e115c8d47..000000000 --- a/ui/src/app/guards/maintenance.guard.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@angular/core' -import { CanActivate, Router, CanActivateChild } from '@angular/router' -import { ServerStatus } from '../services/patch-db/data-model' -import { PatchDbService } from '../services/patch-db/patch-db.service' - -@Injectable({ - providedIn: 'root', -}) -export class MaintenanceGuard implements CanActivate, CanActivateChild { - serverStatus: ServerStatus - - constructor ( - private readonly router: Router, - private readonly patch: PatchDbService, - ) { - this.patch.watch$('server-info', 'status').subscribe(status => { - this.serverStatus = status - }) - } - - canActivate (): boolean { - return this.runServerStatusCheck() - } - - canActivateChild (): boolean { - return this.runServerStatusCheck() - } - - private runServerStatusCheck (): boolean { - if (ServerStatus.BackingUp === this.serverStatus) { - this.router.navigate(['/maintenance'], { replaceUrl: true }) - return false - } else { - return true - } - } -} \ No newline at end of file diff --git a/ui/src/app/guards/unmaintenance.guard.ts b/ui/src/app/guards/unmaintenance.guard.ts deleted file mode 100644 index 7511ef177..000000000 --- a/ui/src/app/guards/unmaintenance.guard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core' -import { CanActivate, Router } from '@angular/router' -import { ServerStatus } from '../services/patch-db/data-model' -import { PatchDbService } from '../services/patch-db/patch-db.service' - -@Injectable({ - providedIn: 'root', -}) -export class UnmaintenanceGuard implements CanActivate { - serverStatus: ServerStatus - - constructor ( - private readonly router: Router, - private readonly patch: PatchDbService, - ) { - this.patch.watch$('server-info', 'status').subscribe(status => { - this.serverStatus = status - }) - } - - canActivate (): boolean { - if (ServerStatus.BackingUp === this.serverStatus) { - return true - } else { - this.router.navigate([''], { replaceUrl: true }) - return false - } - } -} \ No newline at end of file diff --git a/ui/src/app/modals/app-restore/app-restore.component.html b/ui/src/app/modals/app-restore/app-restore.component.html index c68af91f9..ae9f73bee 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.html +++ b/ui/src/app/modals/app-restore/app-restore.component.html @@ -1,95 +1,5 @@ - - - - - - - - Restore From Backup - - - Refresh - - - - - + - - - -

- Select the drive containing the backup you would like to restore. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - {{ loadingError }} - - - - - - - - - - - - No drives found. Insert a backup drive into your Embassy and click "Refresh" above. - - - - - -
- - {{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }} - - - - -

{{ partition.label || partition.logicalname }}

-

{{ partition.capacity | convertBytes }}

-

- - Embassy backups detected - -

-
- - - - -
-
-
- -
-
+
diff --git a/ui/src/app/modals/app-restore/app-restore.component.module.ts b/ui/src/app/modals/app-restore/app-restore.component.module.ts index 482cdfeaa..c9c11f729 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.module.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { AppRestoreComponent } from './app-restore.component' import { SharingModule } from '../../modules/sharing.module' +import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module' @NgModule({ declarations: [AppRestoreComponent], @@ -10,6 +11,7 @@ import { SharingModule } from '../../modules/sharing.module' CommonModule, IonicModule, SharingModule, + BackupDrivesComponentModule, ], exports: [AppRestoreComponent], diff --git a/ui/src/app/modals/app-restore/app-restore.component.ts b/ui/src/app/modals/app-restore/app-restore.component.ts index 5a6d7d023..fa57aeeb2 100644 --- a/ui/src/app/modals/app-restore/app-restore.component.ts +++ b/ui/src/app/modals/app-restore/app-restore.component.ts @@ -1,9 +1,8 @@ import { Component, Input } from '@angular/core' -import { ModalController, IonicSafeString, AlertController } from '@ionic/angular' +import { ModalController, AlertController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component' -import { getErrorMessage } from 'src/app/services/error-toast.service' -import { MappedDiskInfo, MappedPartitionInfo } from 'src/app/util/misc.util' +import { MappedPartitionInfo } from 'src/app/util/misc.util' import { Emver } from 'src/app/services/emver.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { ConfigService } from 'src/app/services/config.service' @@ -15,10 +14,7 @@ import { ConfigService } from 'src/app/services/config.service' }) export class AppRestoreComponent { @Input() pkg: PackageDataEntry - disks: MappedDiskInfo[] - loading = true modal: HTMLIonModalElement - loadingError: string | IonicSafeString constructor ( private readonly modalCtrl: ModalController, @@ -28,40 +24,13 @@ export class AppRestoreComponent { private readonly emver: Emver, ) { } - async ngOnInit () { - this.getExternalDisks() + dismiss () { + this.modalCtrl.dismiss() + } + + async presentModal (partition: MappedPartitionInfo): Promise { this.modal = await this.modalCtrl.getTop() - } - async refresh () { - this.loading = true - await this.getExternalDisks() - } - - async getExternalDisks (): Promise { - try { - const disks = await this.embassyApi.getDisks({ }) - this.disks = disks.map(d => { - const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => { - return { - ...p, - hasBackup: [0, 1].includes(this.emver.compare(p['embassy-os']?.version, '0.3.0')), - backupInfo: null, - } - }) - return { - ...d, - partitions: partionInfo, - } - }) - } catch (e) { - this.loadingError = getErrorMessage(e) - } finally { - this.loading = false - } - } - - async presentModal (logicalname: string): Promise { const modal = await this.modalCtrl.create({ componentProps: { title: 'Decryption Required', @@ -72,7 +41,7 @@ export class AppRestoreComponent { useMask: true, buttonText: 'Restore', loadingText: 'Decrypting drive...', - submitFn: (value: string, loader: HTMLIonLoadingElement) => this.restore(logicalname, value, loader), + submitFn: (value: string, loader: HTMLIonLoadingElement) => this.restore(partition.logicalname, value, loader), }, cssClass: 'alertlike-modal', presentingElement: await this.modalCtrl.getTop(), @@ -86,10 +55,6 @@ export class AppRestoreComponent { await modal.present() } - dismiss () { - this.modalCtrl.dismiss() - } - private async restore (logicalname: string, password: string, loader: HTMLIonLoadingElement): Promise { const { id, title } = this.pkg.manifest @@ -100,7 +65,7 @@ export class AppRestoreComponent { const pkgBackupInfo = backupInfo['package-backups'][id] if (!pkgBackupInfo) { - throw new Error(`Disk does not contain a backup of ${title}`) + throw new Error(`Drive does not contain a backup of ${title}`) } if (this.emver.compare(pkgBackupInfo['os-version'], this.config.version) === 1) { diff --git a/ui/src/app/modals/generic-input/generic-input.component.html b/ui/src/app/modals/generic-input/generic-input.component.html index 6230304d4..33c50e33a 100644 --- a/ui/src/app/modals/generic-input/generic-input.component.html +++ b/ui/src/app/modals/generic-input/generic-input.component.html @@ -19,7 +19,7 @@

{{ label }}

- + diff --git a/ui/src/app/modals/generic-input/generic-input.component.ts b/ui/src/app/modals/generic-input/generic-input.component.ts index a71d13b60..cba486650 100644 --- a/ui/src/app/modals/generic-input/generic-input.component.ts +++ b/ui/src/app/modals/generic-input/generic-input.component.ts @@ -1,5 +1,5 @@ -import { Component, Input } from '@angular/core' -import { ModalController, IonicSafeString, LoadingController } from '@ionic/angular' +import { Component, Input, ViewChild } from '@angular/core' +import { ModalController, IonicSafeString, LoadingController, IonInput } from '@ionic/angular' import { getErrorMessage } from 'src/app/services/error-toast.service' @Component({ @@ -8,6 +8,7 @@ import { getErrorMessage } from 'src/app/services/error-toast.service' styleUrls: ['./generic-input.component.scss'], }) export class GenericInputComponent { + @ViewChild('mainInput', { static: false }) elem: IonInput @Input() title: string @Input() message: string @Input() warning: string @@ -27,6 +28,10 @@ export class GenericInputComponent { private readonly loadingCtrl: LoadingController, ) { } + ngAfterViewInit () { + setTimeout(() => this.elem.setFocus(), 400) + } + toggleMask () { this.unmasked = !this.unmasked } diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index b0c0eda73..557f314a2 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -48,7 +48,9 @@ export class AppListPage { .subscribe(data => { this.loading = false const pkgs = JSON.parse(JSON.stringify(data['package-data'])) as { [id: string]: PackageDataEntry } - this.recoveredPkgs = Object.entries(data['recovered-packages']).map(([id, val]) => { + this.recoveredPkgs = Object.entries(data['recovered-packages']) + .filter(([id, _]) => !pkgs[id]) + .map(([id, val]) => { return { ...val, id, @@ -60,7 +62,7 @@ export class AppListPage { // add known pkgs in preferential order this.order.forEach(id => { if (pkgs[id]) { - this.pkgs.push(this.buildPkg(pkgs[id])) + this.pkgs.push(this.subscribeToPkg(pkgs[id])) delete pkgs[id] } }) @@ -68,7 +70,7 @@ export class AppListPage { // unshift unknown packages and set order in UI DB if (!isEmptyObject(pkgs)) { Object.values(pkgs).forEach(pkg => { - this.pkgs.unshift(this.buildPkg(pkg)) + this.pkgs.unshift(this.subscribeToPkg(pkg)) this.order.unshift(pkg.manifest.id) }) this.setOrder() @@ -78,7 +80,7 @@ export class AppListPage { this.empty = true } - this.subs.push(this.subscribeBoth()) + this.subs.push(this.watchNewlyRecovered()) }) this.subs.push( @@ -164,7 +166,7 @@ export class AppListPage { await alert.present() } - private subscribeBoth (): Subscription { + private watchNewlyRecovered (): Subscription { return combineLatest([this.watchPkgs(), this.patch.watch$('recovered-packages')]) .subscribe(([pkgs, recoveredPkgs]) => { Object.keys(recoveredPkgs).forEach(id => { @@ -186,6 +188,7 @@ export class AppListPage { tap(pkgs => { const ids = Object.keys(pkgs) + // remove uninstalled this.pkgs.forEach((pkg, i) => { const id = pkg.entry.manifest.id if (!ids.includes(id)) { @@ -201,7 +204,7 @@ export class AppListPage { const pkg = this.pkgs.find(p => p.entry.manifest.id === id) if (pkg) return // otherwise add new entry to beginning of array - this.pkgs.unshift(this.buildPkg(pkgs[id])) + this.pkgs.unshift(this.subscribeToPkg(pkgs[id])) }) }), ) @@ -211,7 +214,7 @@ export class AppListPage { this.api.setDbValue({ pointer: '/pkg-order', value: this.order }) } - private buildPkg (pkg: PackageDataEntry): PkgInfo { + private subscribeToPkg (pkg: PackageDataEntry): PkgInfo { const pkgInfo: PkgInfo = { entry: pkg, primaryRendering: PrimaryRendering[renderPkgStatus(pkg).primary], diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/ui/src/app/pages/apps-routes/app-show/app-show.page.html index ee1a7d684..41125288f 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -92,7 +92,7 @@

- + {{ dep.title }}

{{ dep.version | displayEmver }}

@@ -121,7 +121,7 @@ -
+

Downloading: {{ installProgress.downloadProgress }}%

exists(obj)), + ) .subscribe(currentDeps => { - // unsubscribe to deleted - this.dependencies.forEach(dep => { - if (!currentDeps[dep.id]) { - dep.sub.unsubscribe() - } - }) + // remove deleted + this.dependencies.forEach((dep, i) => { + if (!currentDeps[dep.id]) { + dep.sub.unsubscribe() + this.dependencies.splice(i, 1) + } + }) - this.dependencies = Object.keys(currentDeps).map(id => { - const version = this.pkg.manifest.dependencies[id]?.version - if (version) { - const dep = { id, version } as DependencyInfo - dep.sub = this.patch.watch$('package-data', id) - .subscribe(localDep => { - this.setDepValues(dep, localDep) - }) - return dep - } - }).filter(exists) + // subscribe + Object.keys(currentDeps) + .filter(id => { + const inManifest = !!this.pkg.manifest.dependencies[id] + const exists = this.dependencies.find(d => d.id === id) + return inManifest && !exists + }) + .forEach(id => { + const version = this.pkg.manifest.dependencies[id].version + const dep = { id, version } as DependencyInfo + dep.sub = this.patch.watch$('package-data', id) + .subscribe(localDep => { + this.setDepValues(dep, localDep) + }) + this.dependencies.push(dep) + }) }), // 3 this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main') + .pipe( + filter(obj => exists(obj)), + ) .subscribe(main => { if (main.status === PackageMainStatus.Running) { this.healthChecks = { ...main.health } diff --git a/ui/src/app/pages/maintenance/maintenance.module.ts b/ui/src/app/pages/maintenance/maintenance.module.ts deleted file mode 100644 index 5caac9549..000000000 --- a/ui/src/app/pages/maintenance/maintenance.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { MaintenancePage } from './maintenance.page' -import { SharingModule } from 'src/app/modules/sharing.module' - -const routes: Routes = [ - { - path: '', - component: MaintenancePage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - SharingModule, - ], - declarations: [MaintenancePage], -}) -export class MaintenancePageModule { } diff --git a/ui/src/app/pages/maintenance/maintenance.page.html b/ui/src/app/pages/maintenance/maintenance.page.html deleted file mode 100644 index e11322998..000000000 --- a/ui/src/app/pages/maintenance/maintenance.page.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - -

Embassy backing up

- -
-
-
-
- -
\ No newline at end of file diff --git a/ui/src/app/pages/maintenance/maintenance.page.scss b/ui/src/app/pages/maintenance/maintenance.page.scss deleted file mode 100644 index eeedcc997..000000000 --- a/ui/src/app/pages/maintenance/maintenance.page.scss +++ /dev/null @@ -1,4 +0,0 @@ -img { - width: 50%; - max-width: 500px; -} \ No newline at end of file diff --git a/ui/src/app/pages/maintenance/maintenance.page.ts b/ui/src/app/pages/maintenance/maintenance.page.ts deleted file mode 100644 index 04b0549af..000000000 --- a/ui/src/app/pages/maintenance/maintenance.page.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'maintenance', - templateUrl: 'maintenance.page.html', - styleUrls: ['maintenance.page.scss'], -}) -export class MaintenancePage { } - diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts b/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts index 53dd1a7ea..4cf401106 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { ServerBackupPage } from './server-backup.page' import { RouterModule, Routes } from '@angular/router' +import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module' import { SharingModule } from 'src/app/modules/sharing.module' const routes: Routes = [ @@ -18,6 +19,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), SharingModule, + BackupDrivesComponentModule, ], declarations: [ ServerBackupPage, diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.page.html b/ui/src/app/pages/server-routes/server-backup/server-backup.page.html index 4bdd519c5..bb596dd9f 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.page.html +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.page.html @@ -1,88 +1,56 @@ - - - - - - Create Backup - - - Refresh - - - - - + + + + + + + - - - - - -

- Select the drive where you want to create a backup of your Embassy. -

-
-
+ + - - - - - - - - - - - - - - - - + + + + + + Backup Progress + + - - - - - - - - {{ loadingError }} - - - - - - - - - - No drives found. Insert a backup drive into your Embassy and click "Refresh" above. - - - - - -
- - {{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }} - - - + + + + + + + + + -

{{ partition.label || partition.logicalname }}

-

{{ partition.capacity | convertBytes }}

-

- - Embassy backups detected - -

+ {{ pkg.entry.manifest.title }}
+ + + +   + Complete + + + + Backing up +   + + + + + Waiting... +
-
-
-
-
-
-
+ + + + + + + diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index 0099538fa..591ff2c04 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -1,10 +1,12 @@ import { Component } from '@angular/core' -import { ModalController, IonicSafeString } from '@ionic/angular' +import { ModalController, NavController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component' -import { getErrorMessage } from 'src/app/services/error-toast.service' -import { MappedDiskInfo, MappedPartitionInfo } from 'src/app/util/misc.util' -import { Emver } from 'src/app/services/emver.service' +import { MappedPartitionInfo } from 'src/app/util/misc.util' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { PackageDataEntry, PackageMainStatus, ServerStatus } from 'src/app/services/patch-db/data-model' +import { Subscription } from 'rxjs' +import { take } from 'rxjs/operators' @Component({ selector: 'server-backup', @@ -12,68 +14,107 @@ import { Emver } from 'src/app/services/emver.service' styleUrls: ['./server-backup.page.scss'], }) export class ServerBackupPage { - disks: MappedDiskInfo[] - loading = true - loadingError: string | IonicSafeString + backingUp = false + pkgs: PkgInfo[] = [] + subs: Subscription[] constructor ( + private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, - private readonly emver: Emver, private readonly embassyApi: ApiService, + private readonly patch: PatchDbService, ) { } ngOnInit () { - this.getExternalDisks() - } - - async refresh () { - this.loading = true - await this.getExternalDisks() - } - - async getExternalDisks (): Promise { - try { - const disks = await this.embassyApi.getDisks({ }) - this.disks = disks.map(d => { - const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => { - return { - ...p, - hasBackup: [0, 1].includes(this.emver.compare(p['embassy-os']?.version, '0.3.0')), - backupInfo: null, - } - }) - return { - ...d, - partitions: partionInfo, + this.subs = [ + this.patch.watch$('server-info', 'status').subscribe(status => { + if (status === ServerStatus.BackingUp) { + this.backingUp = true + this.subscribeToBackup() + } else { + this.backingUp = false + this.pkgs.forEach(pkg => pkg.sub.unsubscribe()) } - }) - } catch (e) { - this.loadingError = getErrorMessage(e) - } finally { - this.loading = false - } + }), + ] } - async presentModal (logicalname: string): Promise { + ngOnDestroy () { + this.subs.forEach(sub => sub.unsubscribe()) + this.pkgs.forEach(pkg => pkg.sub.unsubscribe()) + } + + back () { + this.navCtrl.back() + } + + async presentModal (partition: MappedPartitionInfo): Promise { + let message: string + if (partition.hasBackup) { + message = 'Enter your master password to decrypt this drive and update its backup. Depending on how much data was added or changed, this could be very fast, or it could take a while.' + } else { + message = 'Enter your master password to create an encrypted backup of your Embassy and all its installed services. Since this is a fresh backup, it could take a while. Future backups will likely be much faster.' + } + const m = await this.modalCtrl.create({ componentProps: { title: 'Create Backup', - message: `Enter your master password to create an encrypted backup of your Embassy and all its installed services.`, + message, label: 'Password', placeholder: 'Enter password', useMask: true, buttonText: 'Create Backup', loadingText: 'Beginning backup...', - submitFn: async (value: string) => await this.create(logicalname, value), + submitFn: async (password: string) => await this.create(partition.logicalname, password), }, cssClass: 'alertlike-modal', component: GenericInputComponent, }) - return await m.present() + await m.present() } private async create (logicalname: string, password: string): Promise { await this.embassyApi.createBackup({ logicalname, password }) } + + private subscribeToBackup () { + this.patch.watch$('package-data') + .pipe( + take(1), + ) + .subscribe(pkgs => { + const pkgArr = Object.values(pkgs) + const activeIndex = pkgArr.findIndex(pkg => pkg.installed.status.main.status === PackageMainStatus.BackingUp) + + this.pkgs = pkgArr.map((pkg, i) => { + const pkgInfo = { + entry: pkg, + active: i === activeIndex, + complete: i < activeIndex, + sub: null, + } + return pkgInfo + }) + + // subscribe to pkg + this.pkgs.forEach(pkg => { + pkg.sub = this.patch.watch$('package-data', pkg.entry.manifest.id, 'installed', 'status', 'main', 'status').subscribe(status => { + if (status === PackageMainStatus.BackingUp) { + pkg.active = true + } else if (pkg.active) { + pkg.active = false + pkg.complete = true + } + }) + }) + }) + } +} + +interface PkgInfo { + entry: PackageDataEntry, + active: boolean + complete: boolean, + sub: Subscription, } diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.html b/ui/src/app/pages/server-routes/server-show/server-show.page.html index f661bebd0..be9ab41f3 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -9,6 +9,20 @@ + Backups + + + +

Create Backup

+

Back up your Embassy and all its services

+

+ + Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }} + +

+
+
+
{{ cat.key }} diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/ui/src/app/pages/server-routes/server-show/server-show.page.ts index b8f50a6ab..a61d0ad7d 100644 --- a/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -105,15 +105,6 @@ export class ServerShowPage { private setButtons (): void { this.settings = { - 'Backups': [ - { - title: 'Create Backup', - description: 'Back up your Embassy and all its services', - icon: 'save-outline', - action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }), - detail: true, - }, - ], 'Insights': [ { title: 'About', diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index feaa85dfd..b2cfdf1cb 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -974,7 +974,7 @@ export module Mock { 'signal-strength': 50, } - export const Disks: RR.GetDisksRes = [ + export const Drives: RR.GetDrivesRes = [ { logicalname: '/dev/sda', model: null, diff --git a/ui/src/app/services/api/api.types.ts b/ui/src/app/services/api/api.types.ts index 6c63312a8..83b9f8683 100644 --- a/ui/src/app/services/api/api.types.ts +++ b/ui/src/app/services/api/api.types.ts @@ -121,10 +121,10 @@ export module RR { export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create export type CreateBackupRes = WithRevision - // disk + // drive - export type GetDisksReq = { } // disk.list - export type GetDisksRes = DiskInfo[] + export type GetDrivesReq = { } // disk.list + export type GetDrivesRes = DriveInfo[] export type GetBackupInfoReq = { logicalname: string, password: string } // disk.backup-info export type GetBackupInfoRes = BackupInfo @@ -287,7 +287,7 @@ export interface SessionMetadata { export type PlatformType = 'cli' | 'ios' | 'ipad' | 'iphone' | 'android' | 'phablet' | 'tablet' | 'cordova' | 'capacitor' | 'electron' | 'pwa' | 'mobile' | 'mobileweb' | 'desktop' | 'hybrid' -export interface DiskInfo { +export interface DriveInfo { logicalname: string vendor: string | null model: string | null @@ -300,10 +300,10 @@ export interface PartitionInfo { label: string | null capacity: number used: number | null - 'embassy-os': EmbassyOsDiskInfo | null + 'embassy-os': EmbassyOsDriveInfo | null } -export interface EmbassyOsDiskInfo { +export interface EmbassyOsDriveInfo { version: string full: boolean } diff --git a/ui/src/app/services/api/embassy-api.service.ts b/ui/src/app/services/api/embassy-api.service.ts index 448914ba2..458b6b4de 100644 --- a/ui/src/app/services/api/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy-api.service.ts @@ -126,9 +126,9 @@ export abstract class ApiService implements Source, Http { () => this.createBackupRaw(params), )() - // disk + // drive - abstract getDisks (params: RR.GetDisksReq): Promise + abstract getDrives (params: RR.GetDrivesReq): Promise abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise diff --git a/ui/src/app/services/api/embassy-live-api.service.ts b/ui/src/app/services/api/embassy-live-api.service.ts index 158f3b516..fda5742ae 100644 --- a/ui/src/app/services/api/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy-live-api.service.ts @@ -191,9 +191,9 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'backup.create', params }) } - // disk + // drives - getDisks (params: RR.GetDisksReq): Promise { + getDrives (params: RR.GetDrivesReq): Promise { return this.http.rpcRequest({ method: 'disk.list', params }) } diff --git a/ui/src/app/services/api/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy-mock-api.service.ts index b59cb534e..c1ed0435d 100644 --- a/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy-mock-api.service.ts @@ -263,7 +263,7 @@ export class MockApiService extends ApiService { async createBackupRaw (params: RR.CreateBackupReq): Promise { await pauseFor(2000) const path = '/server-info/status' - const patch = [ + let patch = [ { op: PatchOp.REPLACE, path, @@ -271,8 +271,37 @@ export class MockApiService extends ApiService { }, ] const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - setTimeout(() => { - const patch = [ + + const ids = ['bitcoind', 'lnd'] + + setTimeout(async () => { + for (let i = 0; i < ids.length; i++) { + const appPath = `/package-data/${ids[i]}/installed/status/main/status` + let appPatch = [ + { + op: PatchOp.REPLACE, + path: appPath, + value: PackageMainStatus.BackingUp, + }, + ] + this.http.rpcRequest>({ method: 'db.patch', params: { patch: appPatch } }) + + await pauseFor(8000) + + appPatch = [ + { + op: PatchOp.REPLACE, + path: appPath, + value: PackageMainStatus.Stopped, + }, + ] + this.http.rpcRequest>({ method: 'db.patch', params: { patch: appPatch } }) + } + + await pauseFor(1000) + + // set server back to running + patch = [ { op: PatchOp.REPLACE, path, @@ -280,15 +309,16 @@ export class MockApiService extends ApiService { }, ] this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - }, this.revertTime) + }, 200) + return res } - // disk + // drives - async getDisks (params: RR.GetDisksReq): Promise { + async getDrives (params: RR.GetDrivesReq): Promise { await pauseFor(2000) - return Mock.Disks + return Mock.Drives } async getBackupInfo (params: RR.GetBackupInfoReq): Promise { diff --git a/ui/src/app/util/misc.util.ts b/ui/src/app/util/misc.util.ts index f9a1d9828..74e737eb3 100644 --- a/ui/src/app/util/misc.util.ts +++ b/ui/src/app/util/misc.util.ts @@ -1,6 +1,6 @@ import { OperatorFunction } from 'rxjs' import { map } from 'rxjs/operators' -import { BackupInfo, DiskInfo, PartitionInfo } from '../services/api/api.types' +import { DriveInfo, PartitionInfo } from '../services/api/api.types' export type Omit = Pick> export type PromiseRes = { result: 'resolve', value: T } | { result: 'reject', value: Error } @@ -193,11 +193,10 @@ export function debounce (delay: number = 300): MethodDecorator { } } -export interface MappedDiskInfo extends DiskInfo { +export interface MappedDriveInfo extends DriveInfo { partitions: MappedPartitionInfo[] } export interface MappedPartitionInfo extends PartitionInfo { hasBackup: boolean - backupInfo: BackupInfo | null } \ No newline at end of file diff --git a/ui/src/global.scss b/ui/src/global.scss index 061902667..8148f1e50 100644 --- a/ui/src/global.scss +++ b/ui/src/global.scss @@ -120,6 +120,13 @@ ion-toast { white-space: normal !important; } +.inline { + * { + display: inline-block; + vertical-align: middle; + } +} + img { border-radius: 100%; } @@ -132,13 +139,6 @@ ion-title { font-family: 'Montserrat'; } -ion-note { - max-width: 140px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - ion-badge { font-weight: bold; }