mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
enable UI during backups, fix state management bugs
This commit is contained in:
committed by
Aiden McClelland
parent
d5dd37b165
commit
896069f1a1
@@ -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),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
<ion-icon name="information-circle-outline"></ion-icon>
|
||||
<ion-icon name="key-outline"></ion-icon>
|
||||
<ion-icon name="list-outline"></ion-icon>
|
||||
<ion-icon name="lock-closed-outline"></ion-icon>
|
||||
<ion-icon name="logo-bitcoin"></ion-icon>
|
||||
<ion-icon name="mail-outline"></ion-icon>
|
||||
<ion-icon name="medkit-outline"></ion-icon>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="close()">
|
||||
<ion-icon [name]="type === 'backup' ? 'arrow-back' : 'close'"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button [disabled]="backupService.loading" (click)="refresh()">
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
@@ -0,0 +1,70 @@
|
||||
<ion-item-group>
|
||||
<!-- always -->
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label>{{ message }}</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="backupService.loading; else loaded">
|
||||
<ion-item-divider>
|
||||
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
||||
</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-avatar slot="start" style="margin-right: 24px;">
|
||||
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 100px; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 50px; height: 16px; margin-bottom: 16px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 100px;"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-template #loaded>
|
||||
|
||||
<!-- error -->
|
||||
<ion-item *ngIf="backupService.loadingError; else noError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ backupService.loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #noError>
|
||||
|
||||
<ion-item *ngIf="!backupService.drives.length; else hasDrives">
|
||||
<ion-label>
|
||||
<ion-text color="warning">
|
||||
No drives found. Insert a backup drive into your Embassy and click "Refresh" above.
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #hasDrives>
|
||||
<ion-item-group>
|
||||
<div *ngFor="let drive of backupService.drives">
|
||||
<ion-item-divider>
|
||||
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }} - {{ drive.capacity | convertBytes }}
|
||||
</ion-item-divider>
|
||||
<ion-item button *ngFor="let partition of drive.partitions" [disabled]="type === 'restore' && !partition.hasBackup" (click)="handleSelect(partition)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partition.hasBackup">
|
||||
<ion-text color="success">
|
||||
Embassy backups detected
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-icon *ngIf="partition.hasBackup" slot="end" color="danger" name="lock-closed-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
@@ -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 { }
|
||||
@@ -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<MappedPartitionInfo> = 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<void> = 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()
|
||||
}
|
||||
}
|
||||
45
ui/src/app/components/backup-drives/backup.service.ts
Normal file
45
ui/src/app/components/backup-drives/backup.service.ts
Normal file
@@ -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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
ion-note {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +1,5 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Restore From Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<backup-drives-header type="restore" (onClose)="dismiss()"></backup-drives-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label>
|
||||
<h2>
|
||||
Select the drive containing the backup you would like to restore.
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="loading">
|
||||
<ion-item-divider>
|
||||
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
||||
</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-avatar slot="start" style="margin-right: 24px;">
|
||||
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 100px; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 50px; height: 16px; margin-bottom: 16px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 100px;"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-container *ngIf="!loading">
|
||||
|
||||
<!-- error -->
|
||||
<ion-item *ngIf="loadingError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no error -->
|
||||
|
||||
<ng-container *ngIf="!loadingError">
|
||||
|
||||
<ion-item *ngIf="!disks.length">
|
||||
<ion-label>
|
||||
<ion-text color="warning">
|
||||
No drives found. Insert a backup drive into your Embassy and click "Refresh" above.
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngIf="disks.length">
|
||||
<div *ngFor="let disk of disks">
|
||||
<ion-item-divider>
|
||||
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }}
|
||||
</ion-item-divider>
|
||||
<ion-item button *ngFor="let partition of disk.partitions" [disabled]="!partition.hasBackup" (click)="presentModal(partition.logicalname)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partition.hasBackup">
|
||||
<ion-text color="success">
|
||||
Embassy backups detected
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="partition.hasBackup">
|
||||
<ion-icon *ngIf="!partition.backupInfo" slot="end" color="danger" name="lock-closed-outline"></ion-icon>
|
||||
<ion-icon *ngIf="partition.backupInfo" slot="end" color="success" name="lock-open-outline"></ion-icon>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<backup-drives type="restore" (onSelect)="presentModal($event)"></backup-drives>
|
||||
</ion-content>
|
||||
|
||||
@@ -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],
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
this.modal = await this.modalCtrl.getTop()
|
||||
}
|
||||
|
||||
async refresh () {
|
||||
this.loading = true
|
||||
await this.getExternalDisks()
|
||||
}
|
||||
|
||||
async getExternalDisks (): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div style="margin: 0 0 24px 16px;">
|
||||
<p class="input-label">{{ label }}</p>
|
||||
<ion-item lines="none" color="dark">
|
||||
<ion-input [type]="useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" [placeholder]="placeholder" (ionChange)="error = ''"></ion-input>
|
||||
<ion-input #mainInput [type]="useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" [placeholder]="placeholder" (ionChange)="error = ''"></ion-input>
|
||||
<ion-button slot="end" *ngIf="useMask" fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="inline" style="font-family: 'Montserrat'">
|
||||
<ion-icon *ngIf="!!dep.errorText" slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
<ion-icon style="padding-right: 4px;" *ngIf="!!dep.errorText" slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
{{ dep.title }}
|
||||
</h2>
|
||||
<p>{{ dep.version | displayEmver }}</p>
|
||||
@@ -121,7 +121,7 @@
|
||||
</ion-item-group>
|
||||
|
||||
<!-- ** installing or updating ** -->
|
||||
<div *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.state" style="padding: 16px;">
|
||||
<div *ngIf="([PackageState.Installing, PackageState.Updating] | includes : pkg.state) && installProgress" style="padding: 16px;">
|
||||
<p>Downloading: {{ installProgress.downloadProgress }}%</p>
|
||||
<ion-progress-bar
|
||||
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
|
||||
|
||||
@@ -11,14 +11,4 @@
|
||||
.icon-spinner {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.inline {
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
ion-icon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { ConnectionFailure, ConnectionService } from 'src/app/services/connectio
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show',
|
||||
@@ -83,29 +84,41 @@ export class AppShowPage {
|
||||
|
||||
// 2
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'current-dependencies')
|
||||
.pipe(
|
||||
filter(obj => 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 }
|
||||
|
||||
@@ -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 { }
|
||||
@@ -1,14 +0,0 @@
|
||||
<ion-content style="--background: black;">
|
||||
|
||||
<ion-grid style="height: 100%;">
|
||||
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
|
||||
<ion-col>
|
||||
<ng-container>
|
||||
<h1>Embassy backing up</h1>
|
||||
<img src="assets/img/gifs/backing-up.gif" />
|
||||
</ng-container>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
</ion-content>
|
||||
@@ -1,4 +0,0 @@
|
||||
img {
|
||||
width: 50%;
|
||||
max-width: 500px;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'maintenance',
|
||||
templateUrl: 'maintenance.page.html',
|
||||
styleUrls: ['maintenance.page.scss'],
|
||||
})
|
||||
export class MaintenancePage { }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,88 +1,56 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Create Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<!-- not backing up -->
|
||||
<ng-container *ngIf="!backingUp">
|
||||
<backup-drives-header type="backup" (onClose)="back()"></backup-drives-header>
|
||||
<ion-content class="ion-padding-top">
|
||||
<backup-drives type="backup" (onSelect)="presentModal($event)"></backup-drives>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group>
|
||||
<!-- always -->
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label>
|
||||
<h2>
|
||||
Select the drive where you want to create a backup of your Embassy.
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- currently backing up -->
|
||||
<ng-container *ngIf="backingUp">
|
||||
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="loading">
|
||||
<ion-item-divider>
|
||||
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
||||
</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-avatar slot="start" style="margin-right: 24px;">
|
||||
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 100px; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 50px; height: 16px; margin-bottom: 16px;"></ion-skeleton-text>
|
||||
<ion-skeleton-text animated style="width: 100px;"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Backup Progress</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-container *ngIf="!loading">
|
||||
|
||||
<!-- error -->
|
||||
<ion-item *ngIf="loadingError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="!loadingError">
|
||||
|
||||
<ion-item *ngIf="!disks.length">
|
||||
<ion-label>
|
||||
<ion-text color="warning">
|
||||
No drives found. Insert a backup drive into your Embassy and click "Refresh" above.
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngIf="disks.length">
|
||||
<div *ngFor="let disk of disks">
|
||||
<ion-item-divider>
|
||||
{{ disk.vendor || 'Unknown Vendor' }} - {{ disk.model || 'Unknown Model' }} - {{ disk.capacity | convertBytes }}
|
||||
</ion-item-divider>
|
||||
<ion-item button *ngFor="let partition of disk.partitions" (click)="presentModal(partition.logicalname)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-content class="ion-padding">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-item-group>
|
||||
<ion-item *ngFor="let pkg of pkgs">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="pkg.entry['static-files'].icon" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partition.hasBackup">
|
||||
<ion-text color="success">
|
||||
Embassy backups detected
|
||||
</ion-text>
|
||||
</p>
|
||||
{{ pkg.entry.manifest.title }}
|
||||
</ion-label>
|
||||
<!-- complete -->
|
||||
<ion-note *ngIf="pkg.complete" class="inline" slot="end">
|
||||
<ion-icon name="checkmark" color="success"></ion-icon>
|
||||
|
||||
<ion-text color="success">Complete</ion-text>
|
||||
</ion-note>
|
||||
<!-- active -->
|
||||
<ion-note *ngIf="pkg.active" class="inline" slot="end">
|
||||
<ion-text color="dark">Backing up</ion-text>
|
||||
|
||||
<ion-spinner color="dark" style="height: 12px; width: 12px;"></ion-spinner>
|
||||
</ion-note>
|
||||
<!-- queued -->
|
||||
<ion-note *ngIf="!pkg.complete && !pkg.active" slot="end">
|
||||
Waiting...
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
|
||||
</ng-container>
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
}
|
||||
|
||||
back () {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
|
||||
async presentModal (partition: MappedPartitionInfo): Promise<void> {
|
||||
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<void> {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,20 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item-group>
|
||||
<ion-item-divider><ion-text color="dark">Backups</ion-text></ion-item-divider>
|
||||
<ion-item button routerLink="backup">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Create Backup</h2>
|
||||
<p>Back up your Embassy and all its services</p>
|
||||
<p>
|
||||
<ion-text color="warning">
|
||||
Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }}
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider>
|
||||
<ion-item [detail]="button.detail" button *ngFor="let button of cat.value" (click)="button.action()">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -121,10 +121,10 @@ export module RR {
|
||||
export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create
|
||||
export type CreateBackupRes = WithRevision<null>
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -126,9 +126,9 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
() => this.createBackupRaw(params),
|
||||
)()
|
||||
|
||||
// disk
|
||||
// drive
|
||||
|
||||
abstract getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes>
|
||||
abstract getDrives (params: RR.GetDrivesReq): Promise<RR.GetDrivesRes>
|
||||
|
||||
abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes>
|
||||
|
||||
|
||||
@@ -191,9 +191,9 @@ export class LiveApiService extends ApiService {
|
||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
// disk
|
||||
// drives
|
||||
|
||||
getDisks (params: RR.GetDisksReq): Promise <RR.GetDisksRes> {
|
||||
getDrives (params: RR.GetDrivesReq): Promise <RR.GetDrivesRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.list', params })
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ export class MockApiService extends ApiService {
|
||||
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||
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<WithRevision<null>>({ 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<WithRevision<null>>({ method: 'db.patch', params: { patch: appPatch } })
|
||||
|
||||
await pauseFor(8000)
|
||||
|
||||
appPatch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: appPath,
|
||||
value: PackageMainStatus.Stopped,
|
||||
},
|
||||
]
|
||||
this.http.rpcRequest<WithRevision<null>>({ 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<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
}, this.revertTime)
|
||||
}, 200)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// disk
|
||||
// drives
|
||||
|
||||
async getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes> {
|
||||
async getDrives (params: RR.GetDrivesReq): Promise<RR.GetDrivesRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.Disks
|
||||
return Mock.Drives
|
||||
}
|
||||
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
|
||||
@@ -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<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>
|
||||
export type PromiseRes<T> = { 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user