mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +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 { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||||
import { AuthGuard } from './guards/auth.guard'
|
import { AuthGuard } from './guards/auth.guard'
|
||||||
import { UnauthGuard } from './guards/unauth.guard'
|
import { UnauthGuard } from './guards/unauth.guard'
|
||||||
import { MaintenanceGuard } from './guards/maintenance.guard'
|
|
||||||
import { UnmaintenanceGuard } from './guards/unmaintenance.guard'
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -18,30 +16,25 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'embassy',
|
path: 'embassy',
|
||||||
canActivate: [AuthGuard, MaintenanceGuard],
|
canActivate: [AuthGuard],
|
||||||
canActivateChild: [AuthGuard, MaintenanceGuard],
|
canActivateChild: [AuthGuard],
|
||||||
loadChildren: () => import('./pages/server-routes/server-routing.module').then(m => m.ServerRoutingModule),
|
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',
|
path: 'marketplace',
|
||||||
canActivate: [AuthGuard, MaintenanceGuard],
|
canActivate: [AuthGuard],
|
||||||
canActivateChild: [AuthGuard, MaintenanceGuard],
|
canActivateChild: [AuthGuard],
|
||||||
loadChildren: () => import('./pages/marketplace-routes/marketplace-routing.module').then(m => m.MarketplaceRoutingModule),
|
loadChildren: () => import('./pages/marketplace-routes/marketplace-routing.module').then(m => m.MarketplaceRoutingModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'notifications',
|
path: 'notifications',
|
||||||
canActivate: [AuthGuard, MaintenanceGuard],
|
canActivate: [AuthGuard],
|
||||||
loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule),
|
loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'services',
|
path: 'services',
|
||||||
canActivate: [AuthGuard, MaintenanceGuard],
|
canActivate: [AuthGuard],
|
||||||
canActivateChild: [AuthGuard, MaintenanceGuard],
|
canActivateChild: [AuthGuard],
|
||||||
loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule),
|
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="information-circle-outline"></ion-icon>
|
||||||
<ion-icon name="key-outline"></ion-icon>
|
<ion-icon name="key-outline"></ion-icon>
|
||||||
<ion-icon name="list-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="logo-bitcoin"></ion-icon>
|
||||||
<ion-icon name="mail-outline"></ion-icon>
|
<ion-icon name="mail-outline"></ion-icon>
|
||||||
<ion-icon name="medkit-outline"></ion-icon>
|
<ion-icon name="medkit-outline"></ion-icon>
|
||||||
|
|||||||
@@ -240,16 +240,6 @@ export class AppComponent {
|
|||||||
private watchStatus (): Subscription {
|
private watchStatus (): Subscription {
|
||||||
return this.patch.watch$('server-info', 'status')
|
return this.patch.watch$('server-info', 'status')
|
||||||
.subscribe(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) {
|
if (status === ServerStatus.Updating) {
|
||||||
this.watchUpdateProgress()
|
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>
|
<backup-drives-header type="restore" (onClose)="dismiss()"></backup-drives-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>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
|
<backup-drives type="restore" (onSelect)="presentModal($event)"></backup-drives>
|
||||||
<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>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { AppRestoreComponent } from './app-restore.component'
|
import { AppRestoreComponent } from './app-restore.component'
|
||||||
import { SharingModule } from '../../modules/sharing.module'
|
import { SharingModule } from '../../modules/sharing.module'
|
||||||
|
import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppRestoreComponent],
|
declarations: [AppRestoreComponent],
|
||||||
@@ -10,6 +11,7 @@ import { SharingModule } from '../../modules/sharing.module'
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
SharingModule,
|
SharingModule,
|
||||||
|
BackupDrivesComponentModule,
|
||||||
],
|
],
|
||||||
exports: [AppRestoreComponent],
|
exports: [AppRestoreComponent],
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
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 { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||||
import { MappedDiskInfo, MappedPartitionInfo } from 'src/app/util/misc.util'
|
|
||||||
import { Emver } from 'src/app/services/emver.service'
|
import { Emver } from 'src/app/services/emver.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
@@ -15,10 +14,7 @@ import { ConfigService } from 'src/app/services/config.service'
|
|||||||
})
|
})
|
||||||
export class AppRestoreComponent {
|
export class AppRestoreComponent {
|
||||||
@Input() pkg: PackageDataEntry
|
@Input() pkg: PackageDataEntry
|
||||||
disks: MappedDiskInfo[]
|
|
||||||
loading = true
|
|
||||||
modal: HTMLIonModalElement
|
modal: HTMLIonModalElement
|
||||||
loadingError: string | IonicSafeString
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
@@ -28,40 +24,13 @@ export class AppRestoreComponent {
|
|||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async ngOnInit () {
|
dismiss () {
|
||||||
this.getExternalDisks()
|
this.modalCtrl.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
async presentModal (partition: MappedPartitionInfo): Promise<void> {
|
||||||
this.modal = await this.modalCtrl.getTop()
|
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({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: 'Decryption Required',
|
title: 'Decryption Required',
|
||||||
@@ -72,7 +41,7 @@ export class AppRestoreComponent {
|
|||||||
useMask: true,
|
useMask: true,
|
||||||
buttonText: 'Restore',
|
buttonText: 'Restore',
|
||||||
loadingText: 'Decrypting drive...',
|
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',
|
cssClass: 'alertlike-modal',
|
||||||
presentingElement: await this.modalCtrl.getTop(),
|
presentingElement: await this.modalCtrl.getTop(),
|
||||||
@@ -86,10 +55,6 @@ export class AppRestoreComponent {
|
|||||||
await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss () {
|
|
||||||
this.modalCtrl.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async restore (logicalname: string, password: string, loader: HTMLIonLoadingElement): Promise<void> {
|
private async restore (logicalname: string, password: string, loader: HTMLIonLoadingElement): Promise<void> {
|
||||||
const { id, title } = this.pkg.manifest
|
const { id, title } = this.pkg.manifest
|
||||||
|
|
||||||
@@ -100,7 +65,7 @@ export class AppRestoreComponent {
|
|||||||
const pkgBackupInfo = backupInfo['package-backups'][id]
|
const pkgBackupInfo = backupInfo['package-backups'][id]
|
||||||
|
|
||||||
if (!pkgBackupInfo) {
|
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) {
|
if (this.emver.compare(pkgBackupInfo['os-version'], this.config.version) === 1) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div style="margin: 0 0 24px 16px;">
|
<div style="margin: 0 0 24px 16px;">
|
||||||
<p class="input-label">{{ label }}</p>
|
<p class="input-label">{{ label }}</p>
|
||||||
<ion-item lines="none" color="dark">
|
<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-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-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { ModalController, IonicSafeString, LoadingController } from '@ionic/angular'
|
import { ModalController, IonicSafeString, LoadingController, IonInput } from '@ionic/angular'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -8,6 +8,7 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
|
|||||||
styleUrls: ['./generic-input.component.scss'],
|
styleUrls: ['./generic-input.component.scss'],
|
||||||
})
|
})
|
||||||
export class GenericInputComponent {
|
export class GenericInputComponent {
|
||||||
|
@ViewChild('mainInput', { static: false }) elem: IonInput
|
||||||
@Input() title: string
|
@Input() title: string
|
||||||
@Input() message: string
|
@Input() message: string
|
||||||
@Input() warning: string
|
@Input() warning: string
|
||||||
@@ -27,6 +28,10 @@ export class GenericInputComponent {
|
|||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
setTimeout(() => this.elem.setFocus(), 400)
|
||||||
|
}
|
||||||
|
|
||||||
toggleMask () {
|
toggleMask () {
|
||||||
this.unmasked = !this.unmasked
|
this.unmasked = !this.unmasked
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ export class AppListPage {
|
|||||||
.subscribe(data => {
|
.subscribe(data => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
const pkgs = JSON.parse(JSON.stringify(data['package-data'])) as { [id: string]: PackageDataEntry }
|
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 {
|
return {
|
||||||
...val,
|
...val,
|
||||||
id,
|
id,
|
||||||
@@ -60,7 +62,7 @@ export class AppListPage {
|
|||||||
// add known pkgs in preferential order
|
// add known pkgs in preferential order
|
||||||
this.order.forEach(id => {
|
this.order.forEach(id => {
|
||||||
if (pkgs[id]) {
|
if (pkgs[id]) {
|
||||||
this.pkgs.push(this.buildPkg(pkgs[id]))
|
this.pkgs.push(this.subscribeToPkg(pkgs[id]))
|
||||||
delete pkgs[id]
|
delete pkgs[id]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -68,7 +70,7 @@ export class AppListPage {
|
|||||||
// unshift unknown packages and set order in UI DB
|
// unshift unknown packages and set order in UI DB
|
||||||
if (!isEmptyObject(pkgs)) {
|
if (!isEmptyObject(pkgs)) {
|
||||||
Object.values(pkgs).forEach(pkg => {
|
Object.values(pkgs).forEach(pkg => {
|
||||||
this.pkgs.unshift(this.buildPkg(pkg))
|
this.pkgs.unshift(this.subscribeToPkg(pkg))
|
||||||
this.order.unshift(pkg.manifest.id)
|
this.order.unshift(pkg.manifest.id)
|
||||||
})
|
})
|
||||||
this.setOrder()
|
this.setOrder()
|
||||||
@@ -78,7 +80,7 @@ export class AppListPage {
|
|||||||
this.empty = true
|
this.empty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subs.push(this.subscribeBoth())
|
this.subs.push(this.watchNewlyRecovered())
|
||||||
})
|
})
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
@@ -164,7 +166,7 @@ export class AppListPage {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeBoth (): Subscription {
|
private watchNewlyRecovered (): Subscription {
|
||||||
return combineLatest([this.watchPkgs(), this.patch.watch$('recovered-packages')])
|
return combineLatest([this.watchPkgs(), this.patch.watch$('recovered-packages')])
|
||||||
.subscribe(([pkgs, recoveredPkgs]) => {
|
.subscribe(([pkgs, recoveredPkgs]) => {
|
||||||
Object.keys(recoveredPkgs).forEach(id => {
|
Object.keys(recoveredPkgs).forEach(id => {
|
||||||
@@ -186,6 +188,7 @@ export class AppListPage {
|
|||||||
tap(pkgs => {
|
tap(pkgs => {
|
||||||
const ids = Object.keys(pkgs)
|
const ids = Object.keys(pkgs)
|
||||||
|
|
||||||
|
// remove uninstalled
|
||||||
this.pkgs.forEach((pkg, i) => {
|
this.pkgs.forEach((pkg, i) => {
|
||||||
const id = pkg.entry.manifest.id
|
const id = pkg.entry.manifest.id
|
||||||
if (!ids.includes(id)) {
|
if (!ids.includes(id)) {
|
||||||
@@ -201,7 +204,7 @@ export class AppListPage {
|
|||||||
const pkg = this.pkgs.find(p => p.entry.manifest.id === id)
|
const pkg = this.pkgs.find(p => p.entry.manifest.id === id)
|
||||||
if (pkg) return
|
if (pkg) return
|
||||||
// otherwise add new entry to beginning of array
|
// 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 })
|
this.api.setDbValue({ pointer: '/pkg-order', value: this.order })
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildPkg (pkg: PackageDataEntry): PkgInfo {
|
private subscribeToPkg (pkg: PackageDataEntry): PkgInfo {
|
||||||
const pkgInfo: PkgInfo = {
|
const pkgInfo: PkgInfo = {
|
||||||
entry: pkg,
|
entry: pkg,
|
||||||
primaryRendering: PrimaryRendering[renderPkgStatus(pkg).primary],
|
primaryRendering: PrimaryRendering[renderPkgStatus(pkg).primary],
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
</ion-thumbnail>
|
</ion-thumbnail>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 class="inline" style="font-family: 'Montserrat'">
|
<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 }}
|
{{ dep.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<p>{{ dep.version | displayEmver }}</p>
|
<p>{{ dep.version | displayEmver }}</p>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
<!-- ** installing or updating ** -->
|
<!-- ** 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>
|
<p>Downloading: {{ installProgress.downloadProgress }}%</p>
|
||||||
<ion-progress-bar
|
<ion-progress-bar
|
||||||
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
|
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
|
||||||
|
|||||||
@@ -11,14 +11,4 @@
|
|||||||
.icon-spinner {
|
.icon-spinner {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 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 { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||||
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
|
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
|
||||||
|
import { filter } from 'rxjs/operators'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show',
|
selector: 'app-show',
|
||||||
@@ -83,29 +84,41 @@ export class AppShowPage {
|
|||||||
|
|
||||||
// 2
|
// 2
|
||||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'current-dependencies')
|
this.patch.watch$('package-data', this.pkgId, 'installed', 'current-dependencies')
|
||||||
|
.pipe(
|
||||||
|
filter(obj => exists(obj)),
|
||||||
|
)
|
||||||
.subscribe(currentDeps => {
|
.subscribe(currentDeps => {
|
||||||
// unsubscribe to deleted
|
// remove deleted
|
||||||
this.dependencies.forEach(dep => {
|
this.dependencies.forEach((dep, i) => {
|
||||||
if (!currentDeps[dep.id]) {
|
if (!currentDeps[dep.id]) {
|
||||||
dep.sub.unsubscribe()
|
dep.sub.unsubscribe()
|
||||||
}
|
this.dependencies.splice(i, 1)
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.dependencies = Object.keys(currentDeps).map(id => {
|
// subscribe
|
||||||
const version = this.pkg.manifest.dependencies[id]?.version
|
Object.keys(currentDeps)
|
||||||
if (version) {
|
.filter(id => {
|
||||||
const dep = { id, version } as DependencyInfo
|
const inManifest = !!this.pkg.manifest.dependencies[id]
|
||||||
dep.sub = this.patch.watch$('package-data', id)
|
const exists = this.dependencies.find(d => d.id === id)
|
||||||
.subscribe(localDep => {
|
return inManifest && !exists
|
||||||
this.setDepValues(dep, localDep)
|
})
|
||||||
})
|
.forEach(id => {
|
||||||
return dep
|
const version = this.pkg.manifest.dependencies[id].version
|
||||||
}
|
const dep = { id, version } as DependencyInfo
|
||||||
}).filter(exists)
|
dep.sub = this.patch.watch$('package-data', id)
|
||||||
|
.subscribe(localDep => {
|
||||||
|
this.setDepValues(dep, localDep)
|
||||||
|
})
|
||||||
|
this.dependencies.push(dep)
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 3
|
// 3
|
||||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||||
|
.pipe(
|
||||||
|
filter(obj => exists(obj)),
|
||||||
|
)
|
||||||
.subscribe(main => {
|
.subscribe(main => {
|
||||||
if (main.status === PackageMainStatus.Running) {
|
if (main.status === PackageMainStatus.Running) {
|
||||||
this.healthChecks = { ...main.health }
|
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 { IonicModule } from '@ionic/angular'
|
||||||
import { ServerBackupPage } from './server-backup.page'
|
import { ServerBackupPage } from './server-backup.page'
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
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'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -18,6 +19,7 @@ const routes: Routes = [
|
|||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
SharingModule,
|
SharingModule,
|
||||||
|
BackupDrivesComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ServerBackupPage,
|
ServerBackupPage,
|
||||||
|
|||||||
@@ -1,88 +1,56 @@
|
|||||||
<ion-header>
|
<!-- not backing up -->
|
||||||
<ion-toolbar>
|
<ng-container *ngIf="!backingUp">
|
||||||
<ion-buttons slot="start">
|
<backup-drives-header type="backup" (onClose)="back()"></backup-drives-header>
|
||||||
<pwa-back-button></pwa-back-button>
|
<ion-content class="ion-padding-top">
|
||||||
</ion-buttons>
|
<backup-drives type="backup" (onSelect)="presentModal($event)"></backup-drives>
|
||||||
<ion-title>Create Backup</ion-title>
|
</ion-content>
|
||||||
<ion-buttons slot="end">
|
</ng-container>
|
||||||
<ion-button (click)="refresh()">
|
|
||||||
Refresh
|
|
||||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<!-- currently backing up -->
|
||||||
<ion-item-group>
|
<ng-container *ngIf="backingUp">
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- loading -->
|
<ion-header>
|
||||||
<ng-container *ngIf="loading">
|
<ion-toolbar>
|
||||||
<ion-item-divider>
|
<ion-buttons slot="start">
|
||||||
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
<pwa-back-button></pwa-back-button>
|
||||||
</ion-item-divider>
|
</ion-buttons>
|
||||||
<ion-item>
|
<ion-title>Backup Progress</ion-title>
|
||||||
<ion-avatar slot="start" style="margin-right: 24px;">
|
</ion-toolbar>
|
||||||
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
</ion-header>
|
||||||
</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 -->
|
<ion-content class="ion-padding">
|
||||||
<ng-container *ngIf="!loading">
|
<ion-grid>
|
||||||
|
<ion-row>
|
||||||
<!-- error -->
|
<ion-col>
|
||||||
<ion-item *ngIf="loadingError">
|
<ion-item-group>
|
||||||
<ion-label>
|
<ion-item *ngFor="let pkg of pkgs">
|
||||||
<ion-text color="danger">
|
<ion-avatar slot="start">
|
||||||
{{ loadingError }}
|
<img [src]="pkg.entry['static-files'].icon" />
|
||||||
</ion-text>
|
</ion-avatar>
|
||||||
</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-label>
|
<ion-label>
|
||||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
{{ pkg.entry.manifest.title }}
|
||||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
|
||||||
<p *ngIf="partition.hasBackup">
|
|
||||||
<ion-text color="success">
|
|
||||||
Embassy backups detected
|
|
||||||
</ion-text>
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
</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>
|
</ion-item>
|
||||||
</div>
|
</ion-item-group>
|
||||||
</ion-item-group>
|
</ion-col>
|
||||||
</ng-container>
|
</ion-row>
|
||||||
</ng-container>
|
</ion-grid>
|
||||||
</ion-item-group>
|
</ion-content>
|
||||||
</ion-content>
|
|
||||||
|
</ng-container>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Component } from '@angular/core'
|
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 { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||||
import { MappedDiskInfo, MappedPartitionInfo } from 'src/app/util/misc.util'
|
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||||
import { Emver } from 'src/app/services/emver.service'
|
import { PackageDataEntry, PackageMainStatus, ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { Subscription } from 'rxjs'
|
||||||
|
import { take } from 'rxjs/operators'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'server-backup',
|
selector: 'server-backup',
|
||||||
@@ -12,68 +14,107 @@ import { Emver } from 'src/app/services/emver.service'
|
|||||||
styleUrls: ['./server-backup.page.scss'],
|
styleUrls: ['./server-backup.page.scss'],
|
||||||
})
|
})
|
||||||
export class ServerBackupPage {
|
export class ServerBackupPage {
|
||||||
disks: MappedDiskInfo[]
|
backingUp = false
|
||||||
loading = true
|
pkgs: PkgInfo[] = []
|
||||||
loadingError: string | IonicSafeString
|
subs: Subscription[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
private readonly navCtrl: NavController,
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly emver: Emver,
|
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
|
private readonly patch: PatchDbService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.getExternalDisks()
|
this.subs = [
|
||||||
}
|
this.patch.watch$('server-info', 'status').subscribe(status => {
|
||||||
|
if (status === ServerStatus.BackingUp) {
|
||||||
async refresh () {
|
this.backingUp = true
|
||||||
this.loading = true
|
this.subscribeToBackup()
|
||||||
await this.getExternalDisks()
|
} else {
|
||||||
}
|
this.backingUp = false
|
||||||
|
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||||
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> {
|
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({
|
const m = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: 'Create Backup',
|
title: 'Create Backup',
|
||||||
message: `Enter your master password to create an encrypted backup of your Embassy and all its installed services.`,
|
message,
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
placeholder: 'Enter password',
|
placeholder: 'Enter password',
|
||||||
useMask: true,
|
useMask: true,
|
||||||
buttonText: 'Create Backup',
|
buttonText: 'Create Backup',
|
||||||
loadingText: 'Beginning 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',
|
cssClass: 'alertlike-modal',
|
||||||
component: GenericInputComponent,
|
component: GenericInputComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
return await m.present()
|
await m.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async create (logicalname: string, password: string): Promise<void> {
|
private async create (logicalname: string, password: string): Promise<void> {
|
||||||
await this.embassyApi.createBackup({ logicalname, password })
|
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-content class="ion-padding">
|
||||||
<ion-item-group>
|
<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">
|
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||||
<ion-item-divider><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider>
|
<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()">
|
<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 {
|
private setButtons (): void {
|
||||||
this.settings = {
|
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': [
|
'Insights': [
|
||||||
{
|
{
|
||||||
title: 'About',
|
title: 'About',
|
||||||
|
|||||||
@@ -974,7 +974,7 @@ export module Mock {
|
|||||||
'signal-strength': 50,
|
'signal-strength': 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Disks: RR.GetDisksRes = [
|
export const Drives: RR.GetDrivesRes = [
|
||||||
{
|
{
|
||||||
logicalname: '/dev/sda',
|
logicalname: '/dev/sda',
|
||||||
model: null,
|
model: null,
|
||||||
|
|||||||
@@ -121,10 +121,10 @@ export module RR {
|
|||||||
export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create
|
export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create
|
||||||
export type CreateBackupRes = WithRevision<null>
|
export type CreateBackupRes = WithRevision<null>
|
||||||
|
|
||||||
// disk
|
// drive
|
||||||
|
|
||||||
export type GetDisksReq = { } // disk.list
|
export type GetDrivesReq = { } // disk.list
|
||||||
export type GetDisksRes = DiskInfo[]
|
export type GetDrivesRes = DriveInfo[]
|
||||||
|
|
||||||
export type GetBackupInfoReq = { logicalname: string, password: string } // disk.backup-info
|
export type GetBackupInfoReq = { logicalname: string, password: string } // disk.backup-info
|
||||||
export type GetBackupInfoRes = BackupInfo
|
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 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
|
logicalname: string
|
||||||
vendor: string | null
|
vendor: string | null
|
||||||
model: string | null
|
model: string | null
|
||||||
@@ -300,10 +300,10 @@ export interface PartitionInfo {
|
|||||||
label: string | null
|
label: string | null
|
||||||
capacity: number
|
capacity: number
|
||||||
used: number | null
|
used: number | null
|
||||||
'embassy-os': EmbassyOsDiskInfo | null
|
'embassy-os': EmbassyOsDriveInfo | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmbassyOsDiskInfo {
|
export interface EmbassyOsDriveInfo {
|
||||||
version: string
|
version: string
|
||||||
full: boolean
|
full: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
|||||||
() => this.createBackupRaw(params),
|
() => 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>
|
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 })
|
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 })
|
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> {
|
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const path = '/server-info/status'
|
const path = '/server-info/status'
|
||||||
const patch = [
|
let patch = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path,
|
||||||
@@ -271,8 +271,37 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
const res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
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,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path,
|
||||||
@@ -280,15 +309,16 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||||
}, this.revertTime)
|
}, 200)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// disk
|
// drives
|
||||||
|
|
||||||
async getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes> {
|
async getDrives (params: RR.GetDrivesReq): Promise<RR.GetDrivesRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return Mock.Disks
|
return Mock.Drives
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OperatorFunction } from 'rxjs'
|
import { OperatorFunction } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
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 Omit<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>
|
||||||
export type PromiseRes<T> = { result: 'resolve', value: T } | { result: 'reject', value: Error }
|
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[]
|
partitions: MappedPartitionInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MappedPartitionInfo extends PartitionInfo {
|
export interface MappedPartitionInfo extends PartitionInfo {
|
||||||
hasBackup: boolean
|
hasBackup: boolean
|
||||||
backupInfo: BackupInfo | null
|
|
||||||
}
|
}
|
||||||
@@ -120,6 +120,13 @@ ion-toast {
|
|||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
* {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
@@ -132,13 +139,6 @@ ion-title {
|
|||||||
font-family: 'Montserrat';
|
font-family: 'Montserrat';
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-note {
|
|
||||||
max-width: 140px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-badge {
|
ion-badge {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user