mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
New recovery flow
This commit is contained in:
committed by
Aiden McClelland
parent
221d99bfee
commit
17d0f9e533
@@ -28,7 +28,7 @@ export class AppComponent {
|
|||||||
handleKeyboardEvent () {
|
handleKeyboardEvent () {
|
||||||
const elems = document.getElementsByClassName('enter-click')
|
const elems = document.getElementsByClassName('enter-click')
|
||||||
const elem = elems[elems.length - 1] as HTMLButtonElement
|
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()
|
if (elem) elem.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-buttons slot="start">
|
<ion-buttons slot="start">
|
||||||
<ion-button (click)="close()">
|
<pwa-back-button></pwa-back-button>
|
||||||
<ion-icon [name]="type === 'backup' ? 'arrow-back' : 'close'"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>{{ title }}</ion-title>
|
<ion-title>{{ title }}</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class BackupDrivesComponent {
|
|||||||
if (this.type === 'backup') {
|
if (this.type === 'backup') {
|
||||||
this.message = 'Select the drive where you want to create a backup of your Embassy.'
|
this.message = 'Select the drive where you want to create a backup of your Embassy.'
|
||||||
} else {
|
} 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()
|
this.backupService.getExternalDrives()
|
||||||
}
|
}
|
||||||
@@ -37,26 +37,13 @@ export class BackupDrivesComponent {
|
|||||||
styleUrls: ['./backup-drives.component.scss'],
|
styleUrls: ['./backup-drives.component.scss'],
|
||||||
})
|
})
|
||||||
export class BackupDrivesHeaderComponent {
|
export class BackupDrivesHeaderComponent {
|
||||||
@Input() type: 'backup' | 'restore'
|
@Input() title: string
|
||||||
@Output() onClose: EventEmitter<void> = new EventEmitter()
|
@Output() onClose: EventEmitter<void> = new EventEmitter()
|
||||||
title: string
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
public readonly backupService: BackupService,
|
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 () {
|
refresh () {
|
||||||
this.backupService.getExternalDrives()
|
this.backupService.getExternalDrives()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export class BackupService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const drives = await this.embassyApi.getDrives({ })
|
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 => {
|
const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => {
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
[style.font-weight]="weight"
|
[style.font-weight]="weight"
|
||||||
>
|
>
|
||||||
{{ disconnected ? 'Unknown' : rendering.display }}
|
{{ 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>
|
<span *ngIf="installProgress">{{ installProgress }}%</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 { }
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -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 { }
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,7 @@ export class EnumListPage {
|
|||||||
this.selectAll = !this.selectAll
|
this.selectAll = !this.selectAll
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleSelected (key: string) {
|
toggleSelected (key: string) {
|
||||||
this.options[key] = !this.options[key]
|
this.options[key] = !this.options[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class GenericInputComponent {
|
|||||||
@Input() useMask = false
|
@Input() useMask = false
|
||||||
@Input() value = ''
|
@Input() value = ''
|
||||||
@Input() loadingText = ''
|
@Input() loadingText = ''
|
||||||
@Input() submitFn: (value: string, loader?: HTMLIonLoadingElement) => Promise<any>
|
@Input() submitFn: (value: string) => Promise<any>
|
||||||
unmasked = false
|
unmasked = false
|
||||||
error: string | IonicSafeString
|
error: string | IonicSafeString
|
||||||
|
|
||||||
@@ -41,7 +41,11 @@ export class GenericInputComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submit () {
|
async submit () {
|
||||||
// @TODO validate input?
|
const value = this.value.trim()
|
||||||
|
|
||||||
|
if (!value && !this.nullable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const loader = await this.loadingCtrl.create({
|
const loader = await this.loadingCtrl.create({
|
||||||
spinner: 'lines',
|
spinner: 'lines',
|
||||||
@@ -51,7 +55,7 @@ export class GenericInputComponent {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.submitFn(this.value, loader)
|
await this.submitFn(value)
|
||||||
this.modalCtrl.dismiss(undefined, 'success')
|
this.modalCtrl.dismiss(undefined, 'success')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = getErrorMessage(e)
|
this.error = getErrorMessage(e)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
|
|||||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.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'
|
import { ActionSuccessPageModule } from 'src/app/modals/action-success/action-success.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -24,7 +23,6 @@ const routes: Routes = [
|
|||||||
QRComponentModule,
|
QRComponentModule,
|
||||||
SharingModule,
|
SharingModule,
|
||||||
GenericFormPageModule,
|
GenericFormPageModule,
|
||||||
AppRestoreComponentModule,
|
|
||||||
ActionSuccessPageModule,
|
ActionSuccessPageModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|||||||
@@ -7,21 +7,11 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<ion-item-group *ngIf="pkg">
|
<ion-item-group *ngIf="pkg">
|
||||||
|
|
||||||
<!-- ** standard actions ** -->
|
<!-- ** standard actions ** -->
|
||||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
<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
|
<app-actions-item
|
||||||
[action]="{
|
[action]="{
|
||||||
name: 'Uninstall',
|
name: 'Uninstall',
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { ActivatedRoute } from '@angular/router'
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
|
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
|
||||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
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 { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
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 { isEmptyObject } from 'src/app/util/misc.util'
|
||||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
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 () {
|
async uninstall () {
|
||||||
const { id, title, version, alerts } = this.pkg.manifest
|
const { id, title, version, alerts } = this.pkg.manifest
|
||||||
const data = await wizardModal(
|
const data = await wizardModal(
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export class AppListPage {
|
|||||||
role: 'cancel',
|
role: 'cancel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Execute',
|
text: 'Delete',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
execute()
|
execute()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- ** installed ** -->
|
<!-- ** installed ** -->
|
||||||
<ng-container *ngIf="pkg.state === PackageState.Installed">
|
<ng-container *ngIf="pkg.state === PackageState.Installed">
|
||||||
<!-- ** !restoring/backing-up ** -->
|
<!-- ** !backing-up ** -->
|
||||||
<ng-container *ngIf="!([PS.BackingUp, PS.Restoring] | includes : statuses.primary)">
|
<ng-container *ngIf="statuses.primary !== PS.BackingUp">
|
||||||
<!-- ** health checks ** -->
|
<!-- ** health checks ** -->
|
||||||
<ng-container *ngIf="statuses.primary === PS.Running && !(healthChecks | empty)">
|
<ng-container *ngIf="statuses.primary === PS.Running && !(healthChecks | empty)">
|
||||||
<ion-item-divider>Health Checks</ion-item-divider>
|
<ion-item-divider>Health Checks</ion-item-divider>
|
||||||
@@ -126,8 +126,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
<!-- ** installing or updating ** -->
|
<!-- ** installing, updating, restoring ** -->
|
||||||
<div *ngIf="([PackageState.Installing, PackageState.Updating] | includes : pkg.state) && installProgress" style="padding: 16px;">
|
<div *ngIf="([PackageState.Installing, PackageState.Updating, PackageState.Restoring] | 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'"
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ export class AppShowPage {
|
|||||||
{
|
{
|
||||||
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
description: `Uninstall, recover from backup, and other commands specific to ${pkgTitle}`,
|
description: `Uninstall and other commands specific to ${pkgTitle}`,
|
||||||
icon: 'flash-outline',
|
icon: 'flash-outline',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 { }
|
||||||
69
ui/src/app/pages/server-routes/restore/restore.component.ts
Normal file
69
ui/src/app/pages/server-routes/restore/restore.component.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<!-- not backing up -->
|
<!-- not backing up -->
|
||||||
<ng-container *ngIf="!backingUp">
|
<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">
|
<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>
|
</ion-content>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from '@angular/core'
|
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 { 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 { MappedPartitionInfo } from 'src/app/util/misc.util'
|
import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||||
@@ -19,7 +19,6 @@ export class ServerBackupPage {
|
|||||||
subs: Subscription[]
|
subs: Subscription[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly navCtrl: NavController,
|
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly patch: PatchDbService,
|
private readonly patch: PatchDbService,
|
||||||
@@ -44,11 +43,7 @@ export class ServerBackupPage {
|
|||||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||||
}
|
}
|
||||||
|
|
||||||
back () {
|
async presentModalPassword (partition: MappedPartitionInfo): Promise<void> {
|
||||||
this.navCtrl.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentModal (partition: MappedPartitionInfo): Promise<void> {
|
|
||||||
let message: string
|
let message: string
|
||||||
if (partition.hasBackup) {
|
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.'
|
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,
|
useMask: true,
|
||||||
buttonText: 'Create Backup',
|
buttonText: 'Create Backup',
|
||||||
loadingText: 'Beginning 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',
|
cssClass: 'alertlike-modal',
|
||||||
component: GenericInputComponent,
|
component: GenericInputComponent,
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const routes: Routes = [
|
|||||||
path: 'preferences',
|
path: 'preferences',
|
||||||
loadChildren: () => import('./preferences/preferences.module').then( m => m.PreferencesPageModule),
|
loadChildren: () => import('./preferences/preferences.module').then( m => m.PreferencesPageModule),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'restore',
|
||||||
|
loadChildren: () => import('./restore/restore.component.module').then( m => m.RestorePageModule),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'sessions',
|
path: 'sessions',
|
||||||
loadChildren: () => import('./sessions/sessions.module').then( m => m.SessionsPageModule),
|
loadChildren: () => import('./sessions/sessions.module').then( m => m.SessionsPageModule),
|
||||||
|
|||||||
@@ -9,20 +9,6 @@
|
|||||||
|
|
||||||
<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,6 +105,22 @@ 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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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': [
|
'Insights': [
|
||||||
{
|
{
|
||||||
title: 'About',
|
title: 'About',
|
||||||
|
|||||||
@@ -990,6 +990,7 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
capacity: 1000000000000,
|
capacity: 1000000000000,
|
||||||
|
internal: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
logicalname: '/dev/sdb',
|
logicalname: '/dev/sdb',
|
||||||
@@ -1015,6 +1016,7 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
capacity: 10000000000,
|
capacity: 10000000000,
|
||||||
|
internal: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1023,10 +1025,17 @@ export module Mock {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
'package-backups': {
|
'package-backups': {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
|
title: 'Bitcoin Core',
|
||||||
version: '0.21.0',
|
version: '0.21.0',
|
||||||
'os-version': '0.3.0',
|
'os-version': '0.3.0',
|
||||||
timestamp: new Date().toISOString(),
|
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: {
|
installed: {
|
||||||
'last-backup': null,
|
'last-backup': null,
|
||||||
status: {
|
status: {
|
||||||
configured: true,
|
configured: false,
|
||||||
main: {
|
main: {
|
||||||
status: PackageMainStatus.Running,
|
status: PackageMainStatus.Stopped,
|
||||||
started: new Date().toISOString(),
|
|
||||||
health: { },
|
|
||||||
},
|
},
|
||||||
'dependency-errors': { },
|
'dependency-errors': { },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ export module RR {
|
|||||||
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
|
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
|
||||||
export type SetPackageConfigRes = WithRevision<null>
|
export type SetPackageConfigRes = WithRevision<null>
|
||||||
|
|
||||||
export type RestorePackageReq = WithExpire<{ id: string, logicalname: string, password: string }> // package.backup.restore
|
export type RestorePackagesReq = WithExpire<{ ids: string[], logicalname: string, password: string }> // package.backup.restore
|
||||||
export type RestorePackageRes = WithRevision<null>
|
export type RestorePackagesRes = WithRevision<null>
|
||||||
|
|
||||||
export type ExecutePackageActionReq = { id: string, 'action-id': string, input?: object } // package.action
|
export type ExecutePackageActionReq = { id: string, 'action-id': string, input?: object } // package.action
|
||||||
export type ExecutePackageActionRes = ActionResponse
|
export type ExecutePackageActionRes = ActionResponse
|
||||||
@@ -300,6 +300,7 @@ export interface DriveInfo {
|
|||||||
model: string | null
|
model: string | null
|
||||||
partitions: PartitionInfo[]
|
partitions: PartitionInfo[]
|
||||||
capacity: number
|
capacity: number
|
||||||
|
internal: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PartitionInfo {
|
export interface PartitionInfo {
|
||||||
@@ -319,14 +320,17 @@ export interface BackupInfo {
|
|||||||
version: string,
|
version: string,
|
||||||
timestamp: string,
|
timestamp: string,
|
||||||
'package-backups': {
|
'package-backups': {
|
||||||
[id: string]: {
|
[id: string]: PackageBackupInfo
|
||||||
version: string
|
|
||||||
'os-version': string
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PackageBackupInfo {
|
||||||
|
title: string
|
||||||
|
version: string
|
||||||
|
'os-version': string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerSpecs {
|
export interface ServerSpecs {
|
||||||
[key: string]: string | number
|
[key: string]: string | number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,9 +154,9 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
|||||||
() => this.setPackageConfigRaw(params),
|
() => this.setPackageConfigRaw(params),
|
||||||
)()
|
)()
|
||||||
|
|
||||||
protected abstract restorePackageRaw (params: RR.RestorePackageReq): Promise<RR.RestorePackageRes>
|
protected abstract restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes>
|
||||||
restorePackage = (params: RR.RestorePackageReq) => this.syncResponse(
|
restorePackages = (params: RR.RestorePackagesReq) => this.syncResponse(
|
||||||
() => this.restorePackageRaw(params),
|
() => this.restorePackagesRaw(params),
|
||||||
)()
|
)()
|
||||||
|
|
||||||
abstract executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes>
|
abstract executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes>
|
||||||
|
|||||||
@@ -236,8 +236,8 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.http.rpcRequest({ method: 'package.config.set', params })
|
return this.http.rpcRequest({ method: 'package.config.set', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async restorePackageRaw (params: RR.RestorePackageReq): Promise <RR.RestorePackageRes> {
|
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise <RR.RestorePackagesRes> {
|
||||||
return this.http.rpcRequest({ method: 'package.restore', params })
|
return this.http.rpcRequest({ method: 'package.backup.restore', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async executePackageAction (params: RR.ExecutePackageActionReq): Promise <RR.ExecutePackageActionRes> {
|
async executePackageAction (params: RR.ExecutePackageActionReq): Promise <RR.ExecutePackageActionRes> {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
|||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
import { HttpService } from '../http.service'
|
import { HttpService } from '../http.service'
|
||||||
import markdown from 'raw-loader!src/assets/markdown/md-sample.md'
|
import markdown from 'raw-loader!src/assets/markdown/md-sample.md'
|
||||||
|
import { Operation } from 'fast-json-patch'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MockApiService extends ApiService {
|
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 } })
|
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)
|
await pauseFor(2000)
|
||||||
const path = `/package-data/${params.id}/installed/status/main/status`
|
const patch: Operation[] = params.ids.map(id => {
|
||||||
const patch = [
|
|
||||||
{
|
const initialProgress: InstallProgress = {
|
||||||
op: PatchOp.REPLACE,
|
size: 120,
|
||||||
path,
|
downloaded: 120,
|
||||||
value: PackageMainStatus.Restoring,
|
'download-complete': true,
|
||||||
},
|
validated: 0,
|
||||||
]
|
'validation-complete': false,
|
||||||
const res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
unpacked: 0,
|
||||||
setTimeout(() => {
|
'unpack-complete': false,
|
||||||
const patch = [
|
}
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
const pkg: PackageDataEntry = {
|
||||||
path,
|
...Mock.LocalPkgs[id],
|
||||||
value: PackageMainStatus.Stopped,
|
state: PackageState.Restoring,
|
||||||
},
|
'install-progress': initialProgress,
|
||||||
]
|
installed: undefined,
|
||||||
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
}
|
||||||
}, this.revertTime)
|
|
||||||
return res
|
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> {
|
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export enum PackageState {
|
|||||||
Installed = 'installed',
|
Installed = 'installed',
|
||||||
Updating = 'updating',
|
Updating = 'updating',
|
||||||
Removing = 'removing',
|
Removing = 'removing',
|
||||||
|
Restoring = 'restoring',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Manifest {
|
export interface Manifest {
|
||||||
@@ -235,7 +236,7 @@ export interface Status {
|
|||||||
'dependency-errors': { [id: string]: DependencyError | null }
|
'dependency-errors': { [id: string]: DependencyError | null }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusRunning | MainStatusBackingUp | MainStatusRestoring
|
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusRunning | MainStatusBackingUp
|
||||||
|
|
||||||
export interface MainStatusStopped {
|
export interface MainStatusStopped {
|
||||||
status: PackageMainStatus.Stopped
|
status: PackageMainStatus.Stopped
|
||||||
@@ -256,17 +257,11 @@ export interface MainStatusBackingUp {
|
|||||||
started: string | null // UTC date string
|
started: string | null // UTC date string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MainStatusRestoring {
|
|
||||||
status: PackageMainStatus.Restoring
|
|
||||||
running: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PackageMainStatus {
|
export enum PackageMainStatus {
|
||||||
Running = 'running',
|
Running = 'running',
|
||||||
Stopping = 'stopping',
|
Stopping = 'stopping',
|
||||||
Stopped = 'stopped',
|
Stopped = 'stopped',
|
||||||
BackingUp = 'backing-up',
|
BackingUp = 'backing-up',
|
||||||
Restoring = 'restoring',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HealthCheckResult = HealthCheckResultStarting |
|
export type HealthCheckResult = HealthCheckResultStarting |
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ export enum PrimaryStatus {
|
|||||||
Installing = 'installing',
|
Installing = 'installing',
|
||||||
Updating = 'updating',
|
Updating = 'updating',
|
||||||
Removing = 'removing',
|
Removing = 'removing',
|
||||||
|
Restoring = 'restoring',
|
||||||
// status
|
// status
|
||||||
Running = 'running',
|
Running = 'running',
|
||||||
Stopping = 'stopping',
|
Stopping = 'stopping',
|
||||||
Stopped = 'stopped',
|
Stopped = 'stopped',
|
||||||
BackingUp = 'backing-up',
|
BackingUp = 'backing-up',
|
||||||
Restoring = 'restoring',
|
|
||||||
// config
|
// config
|
||||||
NeedsConfig = 'needs-config',
|
NeedsConfig = 'needs-config',
|
||||||
}
|
}
|
||||||
@@ -98,10 +98,10 @@ export const PrimaryRendering: { [key: string]: StatusRendering } = {
|
|||||||
[PrimaryStatus.Installing]: { display: 'Installing', color: 'primary', showDots: true },
|
[PrimaryStatus.Installing]: { display: 'Installing', color: 'primary', showDots: true },
|
||||||
[PrimaryStatus.Updating]: { display: 'Updating', color: 'primary', showDots: true },
|
[PrimaryStatus.Updating]: { display: 'Updating', color: 'primary', showDots: true },
|
||||||
[PrimaryStatus.Removing]: { display: 'Removing', color: 'danger', 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.Stopping]: { display: 'Stopping', color: 'dark-shade', showDots: true },
|
||||||
[PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark-shade', showDots: false },
|
[PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark-shade', showDots: false },
|
||||||
[PrimaryStatus.BackingUp]: { display: 'Backing Up', color: 'primary', showDots: true },
|
[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.Running]: { display: 'Running', color: 'success', showDots: false },
|
||||||
[PrimaryStatus.NeedsConfig]: { display: 'Needs Config', color: 'warning' },
|
[PrimaryStatus.NeedsConfig]: { display: 'Needs Config', color: 'warning' },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,18 +289,9 @@ ion-loading {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dots {
|
.dots {
|
||||||
vertical-align: middle;
|
vertical-align: top;
|
||||||
margin-left: 8px;
|
margin-left: 2px;
|
||||||
}
|
margin-right: 2px;
|
||||||
|
|
||||||
.dots-small {
|
|
||||||
width: 12px !important;
|
|
||||||
height: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dots-medium {
|
|
||||||
width: 16px !important;
|
|
||||||
height: 16px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
|||||||
Reference in New Issue
Block a user