New recovery flow

This commit is contained in:
Matt Hill
2021-10-23 20:11:10 -06:00
committed by Aiden McClelland
parent 221d99bfee
commit 17d0f9e533
38 changed files with 358 additions and 283 deletions

View File

@@ -28,7 +28,7 @@ export class AppComponent {
handleKeyboardEvent () {
const elems = document.getElementsByClassName('enter-click')
const elem = elems[elems.length - 1] as HTMLButtonElement
if (!elem || elem.classList.contains('no-click')) return
if (!elem || elem.classList.contains('no-click') || elem.disabled) return
if (elem) elem.click()
}

View File

@@ -1,9 +1,7 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="close()">
<ion-icon [name]="type === 'backup' ? 'arrow-back' : 'close'"></ion-icon>
</ion-button>
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">

View File

@@ -20,7 +20,7 @@ export class BackupDrivesComponent {
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.message = 'Select the drive containing backups you would like to restore.'
}
this.backupService.getExternalDrives()
}
@@ -37,26 +37,13 @@ export class BackupDrivesComponent {
styleUrls: ['./backup-drives.component.scss'],
})
export class BackupDrivesHeaderComponent {
@Input() type: 'backup' | 'restore'
@Input() title: string
@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()
}

View File

@@ -23,7 +23,9 @@ export class BackupService {
try {
const drives = await this.embassyApi.getDrives({ })
this.drives = drives.map(d => {
this.drives = drives
.filter(d => !d.internal)
.map(d => {
const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => {
return {
...p,

View File

@@ -5,6 +5,6 @@
[style.font-weight]="weight"
>
{{ disconnected ? 'Unknown' : rendering.display }}
<ion-spinner *ngIf="rendering.showDots" class="dots dots-small" name="dots"></ion-spinner>
<ion-spinner *ngIf="rendering.showDots" class="dots" name="dots"></ion-spinner>
<span *ngIf="installProgress">{{ installProgress }}%</span>
</p>

View File

@@ -1,3 +0,0 @@
p {
margin: 0;
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { AppRecoverSelectPage } from './app-recover-select.page'
import { FormsModule } from '@angular/forms'
@NgModule({
declarations: [AppRecoverSelectPage],
imports: [
CommonModule,
IonicModule,
FormsModule,
],
exports: [AppRecoverSelectPage],
})
export class AppRecoverSelectPageModule { }

View File

@@ -0,0 +1,42 @@
<ion-header>
<ion-toolbar>
<ion-title>Select Services to Recover</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item-group>
<ion-item *ngFor="let option of options">
<ion-label>
<h2>{{ option.title }}</h2>
<p>Version {{ option.version }}</p>
<p>Backup made: {{ option.timestamp | date : 'short' }}</p>
<p *ngIf="!option.installed && !option['newer-eos']">
<ion-text color="success">Ready to recover</ion-text>
</p>
<p *ngIf="option.installed">
<ion-text color="warning">Unavailable. {{ option.title }} is already installed.</ion-text>
</p>
<p *ngIf="option['newer-eos']">
<ion-text color="danger">Unavailable. Backup was made on a newer version of EmbassyOS.</ion-text>
</p>
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.installed || option['newer-eos']" (ionChange)="handleChange()"></ion-checkbox>
</ion-item>
</ion-item-group>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button [disabled]="!hasSelection" fill="outline" (click)="recover()" class="enter-click">
Recover Selected
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,82 @@
import { Component, Input } from '@angular/core'
import { LoadingController, ModalController, IonicSafeString } from '@ionic/angular'
import { BackupInfo, PackageBackupInfo } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
import { Emver } from 'src/app/services/emver.service'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'app-recover-select',
templateUrl: './app-recover-select.page.html',
styleUrls: ['./app-recover-select.page.scss'],
})
export class AppRecoverSelectPage {
@Input() logicalname: string
@Input() password: string
@Input() backupInfo: BackupInfo
options: (PackageBackupInfo & {
id: string
checked: boolean
installed: boolean
'newer-eos': boolean
})[]
hasSelection = false
error: string | IonicSafeString
constructor (
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly embassyApi: ApiService,
private readonly config: ConfigService,
private readonly emver: Emver,
private readonly patch: PatchDbService,
) { }
ngOnInit () {
this.options = Object.keys(this.backupInfo['package-backups']).map(id => {
return {
...this.backupInfo['package-backups'][id],
id,
checked: false,
installed: !!this.patch.data['package-data'][id],
'newer-eos': this.emver.compare(this.backupInfo['package-backups'][id]['os-version'], this.config.version) === 1,
}
})
}
dismiss () {
this.modalCtrl.dismiss()
}
handleChange () {
this.hasSelection = this.options.some(o => o.checked)
}
async recover (): Promise<void> {
const ids = this.options
.filter(option => !!option.checked)
.map(option => option.id)
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Beginning service recovery...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.restorePackages({
ids,
logicalname: this.logicalname,
password: this.password,
})
this.modalCtrl.dismiss(undefined, 'success')
} catch (e) {
this.error = getErrorMessage(e)
} finally {
loader.dismiss()
}
}
}

View File

@@ -1,5 +0,0 @@
<backup-drives-header type="restore" (onClose)="dismiss()"></backup-drives-header>
<ion-content class="ion-padding-top">
<backup-drives type="restore" (onSelect)="presentModal($event)"></backup-drives>
</ion-content>

View File

@@ -1,19 +0,0 @@
import { NgModule } from '@angular/core'
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],
imports: [
CommonModule,
IonicModule,
SharingModule,
BackupDrivesComponentModule,
],
exports: [AppRestoreComponent],
})
export class AppRestoreComponentModule { }

View File

@@ -1,114 +0,0 @@
import { Component, Input } from '@angular/core'
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 { 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'
@Component({
selector: 'app-restore',
templateUrl: './app-restore.component.html',
styleUrls: ['./app-restore.component.scss'],
})
export class AppRestoreComponent {
@Input() pkg: PackageDataEntry
modal: HTMLIonModalElement
constructor (
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly embassyApi: ApiService,
private readonly config: ConfigService,
private readonly emver: Emver,
) { }
dismiss () {
this.modalCtrl.dismiss()
}
async presentModal (partition: MappedPartitionInfo): Promise<void> {
this.modal = await this.modalCtrl.getTop()
const modal = await this.modalCtrl.create({
componentProps: {
title: 'Decryption Required',
message: 'Enter the password that was originally used to encrypt this backup.',
warning: `Warning! All current data for ${this.pkg.manifest.title} will be overwritten.`,
label: 'Password',
placeholder: 'Enter password',
useMask: true,
buttonText: 'Restore',
loadingText: 'Decrypting drive...',
submitFn: (value: string, loader: HTMLIonLoadingElement) => this.restore(partition.logicalname, value, loader),
},
cssClass: 'alertlike-modal',
presentingElement: await this.modalCtrl.getTop(),
component: GenericInputComponent,
})
modal.onWillDismiss().then(res => {
if (res.role === 'success') this.modal.dismiss(undefined, 'success')
})
await modal.present()
}
private async restore (logicalname: string, password: string, loader: HTMLIonLoadingElement): Promise<void> {
const { id, title } = this.pkg.manifest
const backupInfo = await this.embassyApi.getBackupInfo({
logicalname,
password,
})
const pkgBackupInfo = backupInfo['package-backups'][id]
if (!pkgBackupInfo) {
throw new Error(`Drive does not contain a backup of ${title}`)
}
if (this.emver.compare(pkgBackupInfo['os-version'], this.config.version) === 1) {
throw new Error(`The backup of ${title} you are attempting to restore was made on a newer version of EmbassyOS. Update EmbassyOS and try again.`)
}
const timestamp = new Date(pkgBackupInfo.timestamp).getTime()
const lastBackup = new Date(this.pkg.installed['last-backup']).getTime() // ok if last-backup is null
if (timestamp < lastBackup) {
const proceed = await this.presentAlertNewerBackup()
if (!proceed) {
throw new Error('Action cancelled')
}
}
loader.message = `Beginning Restore of ${title}`
await this.embassyApi.restorePackage({
id,
logicalname,
password,
})
}
private async presentAlertNewerBackup (): Promise<boolean> {
return new Promise(async resolve => {
const alert = await this.alertCtrl.create({
header: 'Outdated Backup',
message: `The backup you are attempting to restore is older than your most recent backup of ${this.pkg.manifest.title}. Are you sure you want to continue?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: () => resolve(false),
},
{
text: 'Continue',
handler: () => resolve(true),
cssClass: 'enter-click',
},
],
})
await alert.present()
})
}
}

View File

@@ -37,7 +37,7 @@ export class EnumListPage {
this.selectAll = !this.selectAll
}
async toggleSelected (key: string) {
toggleSelected (key: string) {
this.options[key] = !this.options[key]
}

View File

@@ -19,7 +19,7 @@ export class GenericInputComponent {
@Input() useMask = false
@Input() value = ''
@Input() loadingText = ''
@Input() submitFn: (value: string, loader?: HTMLIonLoadingElement) => Promise<any>
@Input() submitFn: (value: string) => Promise<any>
unmasked = false
error: string | IonicSafeString
@@ -41,7 +41,11 @@ export class GenericInputComponent {
}
async submit () {
// @TODO validate input?
const value = this.value.trim()
if (!value && !this.nullable) {
return
}
const loader = await this.loadingCtrl.create({
spinner: 'lines',
@@ -51,7 +55,7 @@ export class GenericInputComponent {
await loader.present()
try {
await this.submitFn(this.value, loader)
await this.submitFn(value)
this.modalCtrl.dismiss(undefined, 'success')
} catch (e) {
this.error = getErrorMessage(e)

View File

@@ -6,7 +6,6 @@ import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
import { AppRestoreComponentModule } from 'src/app/modals/app-restore/app-restore.component.module'
import { ActionSuccessPageModule } from 'src/app/modals/action-success/action-success.module'
const routes: Routes = [
@@ -24,7 +23,6 @@ const routes: Routes = [
QRComponentModule,
SharingModule,
GenericFormPageModule,
AppRestoreComponentModule,
ActionSuccessPageModule,
],
declarations: [

View File

@@ -7,21 +7,11 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group *ngIf="pkg">
<!-- ** standard actions ** -->
<ion-item-divider>Standard Actions</ion-item-divider>
<app-actions-item
[action]="{
name: 'Restore From Backup',
description: 'All changes since backup will be lost.',
icon: 'color-wand-outline'
}"
(click)="restore()">
</app-actions-item>
<app-actions-item
[action]="{
name: 'Uninstall',

View File

@@ -3,13 +3,12 @@ import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Action, Manifest, PackageDataEntry, PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { Action, PackageDataEntry, PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Subscription } from 'rxjs'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component'
import { isEmptyObject } from 'src/app/util/misc.util'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
@@ -120,21 +119,6 @@ export class AppActionsPage {
}
}
async restore (): Promise<void> {
const modal = await this.modalCtrl.create({
componentProps: {
pkg: this.pkg,
},
component: AppRestoreComponent,
})
modal.onWillDismiss().then(res => {
if (res.role === 'success') this.navCtrl.back()
})
await modal.present()
}
async uninstall () {
const { id, title, version, alerts } = this.pkg.manifest
const data = await wizardModal(

View File

@@ -155,7 +155,7 @@ export class AppListPage {
role: 'cancel',
},
{
text: 'Execute',
text: 'Delete',
handler: () => {
execute()
},

View File

@@ -49,8 +49,8 @@
</ion-item>
<!-- ** installed ** -->
<ng-container *ngIf="pkg.state === PackageState.Installed">
<!-- ** !restoring/backing-up ** -->
<ng-container *ngIf="!([PS.BackingUp, PS.Restoring] | includes : statuses.primary)">
<!-- ** !backing-up ** -->
<ng-container *ngIf="statuses.primary !== PS.BackingUp">
<!-- ** health checks ** -->
<ng-container *ngIf="statuses.primary === PS.Running && !(healthChecks | empty)">
<ion-item-divider>Health Checks</ion-item-divider>
@@ -126,8 +126,8 @@
</ng-container>
</ion-item-group>
<!-- ** installing or updating ** -->
<div *ngIf="([PackageState.Installing, PackageState.Updating] | includes : pkg.state) && installProgress" style="padding: 16px;">
<!-- ** installing, updating, restoring ** -->
<div *ngIf="([PackageState.Installing, PackageState.Updating, PackageState.Restoring] | includes : pkg.state) && installProgress" style="padding: 16px;">
<p>Downloading: {{ installProgress.downloadProgress }}%</p>
<ion-progress-bar
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"

View File

@@ -369,7 +369,7 @@ export class AppShowPage {
{
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
title: 'Actions',
description: `Uninstall, recover from backup, and other commands specific to ${pkgTitle}`,
description: `Uninstall and other commands specific to ${pkgTitle}`,
icon: 'flash-outline',
color: 'danger',
},

View File

@@ -0,0 +1,5 @@
<backup-drives-header title="Restore From Backup"></backup-drives-header>
<ion-content class="ion-padding-top">
<backup-drives type="restore" (onSelect)="presentModalPassword($event)"></backup-drives>
</ion-content>

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RestorePage } from './restore.component'
import { SharingModule } from 'src/app/modules/sharing.module'
import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module'
import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module'
const routes: Routes = [
{
path: '',
component: RestorePage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
SharingModule,
BackupDrivesComponentModule,
AppRecoverSelectPageModule,
],
declarations: [
RestorePage,
],
})
export class RestorePageModule { }

View File

@@ -0,0 +1,69 @@
import { Component } from '@angular/core'
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 { MappedPartitionInfo } from 'src/app/util/misc.util'
import { BackupInfo } from 'src/app/services/api/api.types'
import { AppRecoverSelectPage } from 'src/app/modals/app-recover-select/app-recover-select.page'
@Component({
selector: 'restore',
templateUrl: './restore.component.html',
styleUrls: ['./restore.component.scss'],
})
export class RestorePage {
constructor (
private readonly modalCtrl: ModalController,
private readonly navCtrl: NavController,
private readonly embassyApi: ApiService,
) { }
async presentModalPassword (partition: MappedPartitionInfo): Promise<void> {
const modal = await this.modalCtrl.create({
componentProps: {
title: 'Decryption Required',
message: 'Enter the password that was originally used to encrypt this backup. After decrypting the drive, you will select the services you want to restore.',
label: 'Password',
placeholder: 'Enter password',
useMask: true,
buttonText: 'Restore',
loadingText: 'Decrypting drive...',
submitFn: (password: string) => this.decryptDrive(partition.logicalname, password),
},
cssClass: 'alertlike-modal',
presentingElement: await this.modalCtrl.getTop(),
component: GenericInputComponent,
})
await modal.present()
}
private async decryptDrive (logicalname: string, password: string): Promise<void> {
const backupInfo = await this.embassyApi.getBackupInfo({
logicalname,
password,
})
this.presentModalSelect(logicalname, password, backupInfo)
}
async presentModalSelect (logicalname: string, password: string, backupInfo: BackupInfo): Promise<void> {
const modal = await this.modalCtrl.create({
componentProps: {
logicalname,
password,
backupInfo,
},
presentingElement: await this.modalCtrl.getTop(),
component: AppRecoverSelectPage,
})
modal.onWillDismiss().then(res => {
if (res.role === 'success') {
this.navCtrl.navigateRoot('/services')
}
})
await modal.present()
}
}

View File

@@ -1,8 +1,8 @@
<!-- not backing up -->
<ng-container *ngIf="!backingUp">
<backup-drives-header type="backup" (onClose)="back()"></backup-drives-header>
<backup-drives-header title="Create Backup"></backup-drives-header>
<ion-content class="ion-padding-top">
<backup-drives type="backup" (onSelect)="presentModal($event)"></backup-drives>
<backup-drives type="backup" (onSelect)="presentModalPassword($event)"></backup-drives>
</ion-content>
</ng-container>

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { ModalController } 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 { MappedPartitionInfo } from 'src/app/util/misc.util'
@@ -19,7 +19,6 @@ export class ServerBackupPage {
subs: Subscription[]
constructor (
private readonly navCtrl: NavController,
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
private readonly patch: PatchDbService,
@@ -44,11 +43,7 @@ export class ServerBackupPage {
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
}
back () {
this.navCtrl.back()
}
async presentModal (partition: MappedPartitionInfo): Promise<void> {
async presentModalPassword (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.'
@@ -65,7 +60,7 @@ export class ServerBackupPage {
useMask: true,
buttonText: 'Create Backup',
loadingText: 'Beginning backup...',
submitFn: async (password: string) => await this.create(partition.logicalname, password),
submitFn: (password: string) => this.create(partition.logicalname, password),
},
cssClass: 'alertlike-modal',
component: GenericInputComponent,

View File

@@ -26,6 +26,10 @@ const routes: Routes = [
path: 'preferences',
loadChildren: () => import('./preferences/preferences.module').then( m => m.PreferencesPageModule),
},
{
path: 'restore',
loadChildren: () => import('./restore/restore.component.module').then( m => m.RestorePageModule),
},
{
path: 'sessions',
loadChildren: () => import('./sessions/sessions.module').then( m => m.SessionsPageModule),

View File

@@ -9,20 +9,6 @@
<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()">

View File

@@ -105,6 +105,22 @@ 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,
},
{
title: 'Restore From Backup',
description: 'Restore one or more services from a prior backup',
icon: 'color-wand-outline',
action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
detail: true,
},
],
'Insights': [
{
title: 'About',

View File

@@ -990,6 +990,7 @@ export module Mock {
},
],
capacity: 1000000000000,
internal: true,
},
{
logicalname: '/dev/sdb',
@@ -1015,6 +1016,7 @@ export module Mock {
},
],
capacity: 10000000000,
internal: false,
},
]
@@ -1023,10 +1025,17 @@ export module Mock {
timestamp: new Date().toISOString(),
'package-backups': {
bitcoind: {
title: 'Bitcoin Core',
version: '0.21.0',
'os-version': '0.3.0',
timestamp: new Date().toISOString(),
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
version: '0.2.2',
'os-version': '0.3.0',
timestamp: new Date().toISOString(),
},
},
}
@@ -1614,11 +1623,9 @@ export module Mock {
installed: {
'last-backup': null,
status: {
configured: true,
configured: false,
main: {
status: PackageMainStatus.Running,
started: new Date().toISOString(),
health: { },
status: PackageMainStatus.Stopped,
},
'dependency-errors': { },
},

View File

@@ -157,8 +157,8 @@ export module RR {
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
export type SetPackageConfigRes = WithRevision<null>
export type RestorePackageReq = WithExpire<{ id: string, logicalname: string, password: string }> // package.backup.restore
export type RestorePackageRes = WithRevision<null>
export type RestorePackagesReq = WithExpire<{ ids: string[], logicalname: string, password: string }> // package.backup.restore
export type RestorePackagesRes = WithRevision<null>
export type ExecutePackageActionReq = { id: string, 'action-id': string, input?: object } // package.action
export type ExecutePackageActionRes = ActionResponse
@@ -300,6 +300,7 @@ export interface DriveInfo {
model: string | null
partitions: PartitionInfo[]
capacity: number
internal: boolean
}
export interface PartitionInfo {
@@ -319,14 +320,17 @@ export interface BackupInfo {
version: string,
timestamp: string,
'package-backups': {
[id: string]: {
version: string
'os-version': string
timestamp: string
}
[id: string]: PackageBackupInfo
}
}
export interface PackageBackupInfo {
title: string
version: string
'os-version': string
timestamp: string
}
export interface ServerSpecs {
[key: string]: string | number
}

View File

@@ -154,9 +154,9 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
() => this.setPackageConfigRaw(params),
)()
protected abstract restorePackageRaw (params: RR.RestorePackageReq): Promise<RR.RestorePackageRes>
restorePackage = (params: RR.RestorePackageReq) => this.syncResponse(
() => this.restorePackageRaw(params),
protected abstract restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes>
restorePackages = (params: RR.RestorePackagesReq) => this.syncResponse(
() => this.restorePackagesRaw(params),
)()
abstract executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes>

View File

@@ -236,8 +236,8 @@ export class LiveApiService extends ApiService {
return this.http.rpcRequest({ method: 'package.config.set', params })
}
async restorePackageRaw (params: RR.RestorePackageReq): Promise <RR.RestorePackageRes> {
return this.http.rpcRequest({ method: 'package.restore', params })
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise <RR.RestorePackagesRes> {
return this.http.rpcRequest({ method: 'package.backup.restore', params })
}
async executePackageAction (params: RR.ExecutePackageActionReq): Promise <RR.ExecutePackageActionRes> {

View File

@@ -8,6 +8,7 @@ import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { Mock } from './api.fixures'
import { HttpService } from '../http.service'
import markdown from 'raw-loader!src/assets/markdown/md-sample.md'
import { Operation } from 'fast-json-patch'
@Injectable()
export class MockApiService extends ApiService {
@@ -413,28 +414,38 @@ export class MockApiService extends ApiService {
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
}
async restorePackageRaw (params: RR.RestorePackageReq): Promise<RR.RestorePackageRes> {
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes> {
await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main/status`
const patch = [
{
op: PatchOp.REPLACE,
path,
value: PackageMainStatus.Restoring,
},
]
const res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
setTimeout(() => {
const patch = [
{
op: PatchOp.REPLACE,
path,
value: PackageMainStatus.Stopped,
},
]
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
}, this.revertTime)
return res
const patch: Operation[] = params.ids.map(id => {
const initialProgress: InstallProgress = {
size: 120,
downloaded: 120,
'download-complete': true,
validated: 0,
'validation-complete': false,
unpacked: 0,
'unpack-complete': false,
}
const pkg: PackageDataEntry = {
...Mock.LocalPkgs[id],
state: PackageState.Restoring,
'install-progress': initialProgress,
installed: undefined,
}
setTimeout(async () => {
this.updateProgress(id, initialProgress)
}, 2000)
return {
op: 'add',
path: `/package-data/${id}`,
value: pkg,
}
})
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
}
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {

View File

@@ -95,6 +95,7 @@ export enum PackageState {
Installed = 'installed',
Updating = 'updating',
Removing = 'removing',
Restoring = 'restoring',
}
export interface Manifest {
@@ -235,7 +236,7 @@ export interface Status {
'dependency-errors': { [id: string]: DependencyError | null }
}
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusRunning | MainStatusBackingUp | MainStatusRestoring
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusRunning | MainStatusBackingUp
export interface MainStatusStopped {
status: PackageMainStatus.Stopped
@@ -256,17 +257,11 @@ export interface MainStatusBackingUp {
started: string | null // UTC date string
}
export interface MainStatusRestoring {
status: PackageMainStatus.Restoring
running: boolean
}
export enum PackageMainStatus {
Running = 'running',
Stopping = 'stopping',
Stopped = 'stopped',
BackingUp = 'backing-up',
Restoring = 'restoring',
}
export type HealthCheckResult = HealthCheckResultStarting |

View File

@@ -71,12 +71,12 @@ export enum PrimaryStatus {
Installing = 'installing',
Updating = 'updating',
Removing = 'removing',
Restoring = 'restoring',
// status
Running = 'running',
Stopping = 'stopping',
Stopped = 'stopped',
BackingUp = 'backing-up',
Restoring = 'restoring',
// config
NeedsConfig = 'needs-config',
}
@@ -98,10 +98,10 @@ export const PrimaryRendering: { [key: string]: StatusRendering } = {
[PrimaryStatus.Installing]: { display: 'Installing', color: 'primary', showDots: true },
[PrimaryStatus.Updating]: { display: 'Updating', color: 'primary', showDots: true },
[PrimaryStatus.Removing]: { display: 'Removing', color: 'danger', showDots: true },
[PrimaryStatus.Restoring]: { display: 'Restoring', color: 'primary', showDots: true },
[PrimaryStatus.Stopping]: { display: 'Stopping', color: 'dark-shade', showDots: true },
[PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark-shade', showDots: false },
[PrimaryStatus.BackingUp]: { display: 'Backing Up', color: 'primary', showDots: true },
[PrimaryStatus.Restoring]: { display: 'Restoring', color: 'primary', showDots: true },
[PrimaryStatus.Running]: { display: 'Running', color: 'success', showDots: false },
[PrimaryStatus.NeedsConfig]: { display: 'Needs Config', color: 'warning' },
}

View File

@@ -289,18 +289,9 @@ ion-loading {
}
.dots {
vertical-align: middle;
margin-left: 8px;
}
.dots-small {
width: 12px !important;
height: 12px !important;
}
.dots-medium {
width: 16px !important;
height: 16px !important;
vertical-align: top;
margin-left: 2px;
margin-right: 2px;
}
h2 {