From 2c5aa84fe74ab97e45981dc634ad4066c2a4c661 Mon Sep 17 00:00:00 2001
From: Matt Hill
Date: Tue, 28 Jun 2022 12:14:26 -0600
Subject: [PATCH] selective backups and better drive selection interface
(#1576)
* selective backups and better drive selection interface
* fix disabled checkbox and backup drives menu styling
* feat: package-ids
* only show services that are backing up on backup page
* refactor for performace and cleanliness
Co-authored-by: Matt Hill
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
Co-authored-by: J M
---
backend/src/backup/backup_bulk.rs | 86 ++++++++--
backend/src/db/model.rs | 10 +-
backend/src/init.rs | 2 +-
.../app/app/preloader/preloader.component.ts | 2 +-
.../backup-drives.component.html | 13 +-
.../backup-drives.component.scss | 18 ++
.../backup-drives/backup-drives.component.ts | 14 +-
.../backup-select/backup-select.module.ts | 12 ++
.../backup-select/backup-select.page.html | 45 +++++
.../backup-select/backup-select.page.scss | 0
.../backup-select/backup-select.page.ts | 61 +++++++
.../developer-list/developer-list.page.html | 2 +-
.../marketplaces/marketplaces.page.ts | 13 +-
.../backing-up/backing-up.component.html | 63 +++++++
.../backing-up/backing-up.component.ts | 50 ++++++
.../server-backup/server-backup.module.ts | 6 +-
.../server-backup/server-backup.page.html | 63 ++-----
.../server-backup/server-backup.page.ts | 159 ++++++------------
.../server-show/server-show.page.html | 6 +-
.../ui/src/app/services/api/api.fixures.ts | 2 +-
.../ui/src/app/services/api/api.types.ts | 1 +
.../services/api/embassy-mock-api.service.ts | 28 ++-
.../ui/src/app/services/api/mock-patch.ts | 2 +-
.../ui/src/app/services/eos.service.ts | 16 +-
.../src/app/services/patch-db/data-model.ts | 6 +-
25 files changed, 460 insertions(+), 220 deletions(-)
create mode 100644 frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts
create mode 100644 frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html
create mode 100644 frontend/projects/ui/src/app/modals/backup-select/backup-select.page.scss
create mode 100644 frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts
create mode 100644 frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html
create mode 100644 frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts
diff --git a/backend/src/backup/backup_bulk.rs b/backend/src/backup/backup_bulk.rs
index d49d2cd4c..1a433829a 100644
--- a/backend/src/backup/backup_bulk.rs
+++ b/backend/src/backup/backup_bulk.rs
@@ -1,7 +1,8 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use chrono::Utc;
+use clap::ArgMatches;
use color_eyre::eyre::eyre;
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
@@ -18,6 +19,7 @@ use super::PackageBackupReport;
use crate::auth::check_password_against_db;
use crate::backup::{BackupReport, ServerBackupReport};
use crate::context::RpcContext;
+use crate::db::model::BackupProgress;
use crate::db::util::WithRevision;
use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite;
@@ -112,12 +114,24 @@ impl Serialize for OsBackup {
}
}
+fn parse_comma_separated(arg: &str, _: &ArgMatches<'_>) -> Result, Error> {
+ arg.split(',')
+ .map(|s| s.trim().parse().map_err(Error::from))
+ .collect()
+}
+
#[command(rename = "create", display(display_none))]
#[instrument(skip(ctx, old_password, password))]
pub async fn backup_all(
#[context] ctx: RpcContext,
#[arg(rename = "target-id")] target_id: BackupTargetId,
#[arg(rename = "old-password", long = "old-password")] old_password: Option,
+ #[arg(
+ rename = "package-ids",
+ long = "package-ids",
+ parse(parse_comma_separated)
+ )]
+ service_ids: Option>,
#[arg] password: String,
) -> Result, Error> {
let mut db = ctx.db.handle();
@@ -130,17 +144,27 @@ pub async fn backup_all(
old_password.as_ref().unwrap_or(&password),
)
.await?;
+ let all_packages = crate::db::DatabaseModel::new()
+ .package_data()
+ .get(&mut db, false)
+ .await?
+ .0
+ .keys()
+ .into_iter()
+ .cloned()
+ .collect();
+ let service_ids = service_ids.unwrap_or(all_packages);
if old_password.is_some() {
backup_guard.change_password(&password)?;
}
- let revision = assure_backing_up(&mut db).await?;
+ let revision = assure_backing_up(&mut db, &service_ids).await?;
tokio::task::spawn(async move {
let backup_res = perform_backup(&ctx, &mut db, backup_guard).await;
- let status_model = crate::db::DatabaseModel::new()
+ let backup_progress = crate::db::DatabaseModel::new()
.server_info()
.status_info()
- .backing_up();
- status_model
+ .backup_progress();
+ backup_progress
.clone()
.lock(&mut db, LockType::Write)
.await
@@ -207,8 +231,8 @@ pub async fn backup_all(
.expect("failed to send notification");
}
}
- status_model
- .put(&mut db, &false)
+ backup_progress
+ .delete(&mut db)
.await
.expect("failed to change server status");
});
@@ -218,23 +242,40 @@ pub async fn backup_all(
})
}
-#[instrument(skip(db))]
-async fn assure_backing_up(db: &mut PatchDbHandle) -> Result
Path: {{ cifs.path }}
+
+
+
diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss
index e69de29bb..36da157ea 100644
--- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss
+++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.scss
@@ -0,0 +1,18 @@
+.click-area {
+ padding: 50px;
+
+
+ &:hover {
+ background-color: var(--ion-color-medium-tint);
+ }
+
+ ion-icon {
+ font-size: 27px;
+ }
+}
+
+@media (max-width: 1000px) {
+ .click-area {
+ padding: 18px 0px 10px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts
index 926abbba6..3d73d49a6 100644
--- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts
+++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts
@@ -89,9 +89,12 @@ export class BackupDrivesComponent {
}
async presentActionCifs(
+ event: Event,
target: MappedBackupTarget,
index: number,
): Promise {
+ event.stopPropagation()
+
const entry = target.entry as CifsBackupTarget
const action = await this.actionCtrl.create({
@@ -114,17 +117,6 @@ export class BackupDrivesComponent {
this.presentModalEditCifs(target.id, entry, index)
},
},
- {
- text:
- this.type === 'create' ? 'Create Backup' : 'Restore From Backup',
- icon:
- this.type === 'create'
- ? 'cloud-upload-outline'
- : 'cloud-download-outline',
- handler: () => {
- this.select(target)
- },
- },
],
})
diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts b/frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts
new file mode 100644
index 000000000..be840eff2
--- /dev/null
+++ b/frontend/projects/ui/src/app/modals/backup-select/backup-select.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core'
+import { CommonModule } from '@angular/common'
+import { IonicModule } from '@ionic/angular'
+import { BackupSelectPage } from './backup-select.page'
+import { FormsModule } from '@angular/forms'
+
+@NgModule({
+ declarations: [BackupSelectPage],
+ imports: [CommonModule, IonicModule, FormsModule],
+ exports: [BackupSelectPage],
+})
+export class BackupSelectPageModule {}
diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html
new file mode 100644
index 000000000..8ad5ff221
--- /dev/null
+++ b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.html
@@ -0,0 +1,45 @@
+
+
+ Select Services to Back Up
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pkg.title }}
+
+
+
+
+
+
+
+
+
+
+ Back Up Selected
+
+
+
+
diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.scss b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts
new file mode 100644
index 000000000..78a2de8e3
--- /dev/null
+++ b/frontend/projects/ui/src/app/modals/backup-select/backup-select.page.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core'
+import { ModalController, IonicSafeString } from '@ionic/angular'
+import { map, take } from 'rxjs/operators'
+import { PackageState } from 'src/app/services/patch-db/data-model'
+import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
+
+@Component({
+ selector: 'backup-select',
+ templateUrl: './backup-select.page.html',
+ styleUrls: ['./backup-select.page.scss'],
+})
+export class BackupSelectPage {
+ hasSelection = false
+ error: string | IonicSafeString = ''
+ pkgs: {
+ id: string
+ title: string
+ icon: string
+ disabled: boolean
+ checked: boolean
+ }[] = []
+
+ constructor(
+ private readonly modalCtrl: ModalController,
+ private readonly patch: PatchDbService,
+ ) {}
+
+ ngOnInit() {
+ this.patch
+ .watch$('package-data')
+ .pipe(
+ map(pkgs => {
+ return Object.values(pkgs).map(pkg => {
+ const { id, title } = pkg.manifest
+ return {
+ id,
+ title,
+ icon: pkg['static-files'].icon,
+ disabled: pkg.state !== PackageState.Installed,
+ checked: pkg.state === PackageState.Installed,
+ }
+ })
+ }),
+ take(1),
+ )
+ .subscribe(pkgs => (this.pkgs = pkgs))
+ }
+
+ dismiss(success = false) {
+ if (success) {
+ const ids = this.pkgs.filter(p => p.checked).map(p => p.id)
+ this.modalCtrl.dismiss(ids)
+ } else {
+ this.modalCtrl.dismiss()
+ }
+ }
+
+ handleChange() {
+ this.hasSelection = this.pkgs.some(p => p.checked)
+ }
+}
diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html
index 4fecacf35..e00330448 100644
--- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html
+++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html
@@ -31,7 +31,7 @@
fill="clear"
(click)="presentAction(entry.key, $event)"
>
-
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts
index b08d56900..992ce934c 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts
+++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts
@@ -5,7 +5,7 @@ import {
ModalController,
} from '@ionic/angular'
import { ActionSheetButton } from '@ionic/core'
-import { ErrorToastService } from '@start9labs/shared'
+import { DestroyService, ErrorToastService } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
@@ -15,7 +15,12 @@ import { v4 } from 'uuid'
import { UIMarketplaceData } from '../../../services/patch-db/data-model'
import { ConfigService } from '../../../services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
-import { distinctUntilChanged, finalize, first } from 'rxjs/operators'
+import {
+ distinctUntilChanged,
+ finalize,
+ first,
+ takeUntil,
+} from 'rxjs/operators'
type Marketplaces = {
id: string | undefined
@@ -27,6 +32,7 @@ type Marketplaces = {
selector: 'marketplaces',
templateUrl: 'marketplaces.page.html',
styleUrls: ['marketplaces.page.scss'],
+ providers: [DestroyService],
})
export class MarketplacesPage {
selectedId: string | undefined
@@ -42,12 +48,13 @@ export class MarketplacesPage {
private readonly marketplaceService: MarketplaceService,
private readonly config: ConfigService,
public readonly patch: PatchDbService,
+ private readonly destroy$: DestroyService,
) {}
ngOnInit() {
this.patch
.watch$('ui', 'marketplace')
- .pipe(distinctUntilChanged())
+ .pipe(distinctUntilChanged(), takeUntil(this.destroy$))
.subscribe((mp: UIMarketplaceData | undefined) => {
let marketplaces: Marketplaces = [
{
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html
new file mode 100644
index 000000000..536043f69
--- /dev/null
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Backup Progress
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pkg.value.manifest.title }}
+
+
+
+
+ Complete
+
+
+
+
+
+
+
+ Backing up
+
+
+
+ Waiting...
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts
new file mode 100644
index 000000000..fb50d09e4
--- /dev/null
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/backing-up/backing-up.component.ts
@@ -0,0 +1,50 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Pipe,
+ PipeTransform,
+} from '@angular/core'
+import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
+import { take } from 'rxjs/operators'
+import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
+import { EOSService } from 'src/app/services/eos.service'
+import { Observable } from 'rxjs'
+
+@Component({
+ selector: 'backing-up',
+ templateUrl: './backing-up.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class BackingUpComponent {
+ readonly pkgs$ = this.patch.watch$('package-data').pipe(take(1))
+ readonly backupProgress$ = this.patch.watch$(
+ 'server-info',
+ 'status-info',
+ 'backup-progress',
+ )
+
+ PackageMainStatus = PackageMainStatus
+
+ constructor(
+ public readonly eosService: EOSService,
+ public readonly patch: PatchDbService,
+ ) {}
+}
+
+@Pipe({
+ name: 'pkgMainStatus',
+})
+export class PkgMainStatusPipe implements PipeTransform {
+ transform(pkgId: string): Observable {
+ return this.patch.watch$(
+ 'package-data',
+ pkgId,
+ 'installed',
+ 'status',
+ 'main',
+ 'status',
+ )
+ }
+
+ constructor(private readonly patch: PatchDbService) {}
+}
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts
index d4262392d..6a1782985 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.module.ts
@@ -2,9 +2,12 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { ServerBackupPage } from './server-backup.page'
+import { BackingUpComponent } from './backing-up/backing-up.component'
import { RouterModule, Routes } from '@angular/router'
import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module'
import { SharedPipesModule } from '@start9labs/shared'
+import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module'
+import { PkgMainStatusPipe } from './backing-up/backing-up.component'
const routes: Routes = [
{
@@ -20,7 +23,8 @@ const routes: Routes = [
RouterModule.forChild(routes),
SharedPipesModule,
BackupDrivesComponentModule,
+ BackupSelectPageModule,
],
- declarations: [ServerBackupPage],
+ declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe],
})
export class ServerBackupPageModule {}
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html
index 3b201d3e2..c4576f3f1 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.html
@@ -1,55 +1,16 @@
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
- Backup Progress
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ pkg.entry.manifest.title }}
-
-
-
-
-
- Complete
-
-
-
-
- Backing up
-
-
-
- Waiting...
-
-
-
-
-
-
-
-
-
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts
index 559e931b7..1bfd479b7 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts
@@ -10,78 +10,73 @@ import {
GenericInputOptions,
} from 'src/app/modals/generic-input/generic-input.component'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
-import { Subscription } from 'rxjs'
-import { take } from 'rxjs/operators'
+import { skip, takeUntil } from 'rxjs/operators'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
-import {
- PackageDataEntry,
- PackageMainStatus,
-} from 'src/app/services/patch-db/data-model'
import * as argon2 from '@start9labs/argon2'
import {
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
+import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.page'
+import { EOSService } from 'src/app/services/eos.service'
+import { DestroyService } from '@start9labs/shared'
@Component({
selector: 'server-backup',
templateUrl: './server-backup.page.html',
styleUrls: ['./server-backup.page.scss'],
+ providers: [DestroyService],
})
export class ServerBackupPage {
- backingUp = false
- pkgs: PkgInfo[] = []
- subs: Subscription[]
+ target: MappedBackupTarget
+ serviceIds: string[] = []
+
+ readonly backingUp$ = this.eosService.backingUp$
constructor(
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
- private readonly patch: PatchDbService,
private readonly navCtrl: NavController,
+ private readonly destroy$: DestroyService,
+ private readonly eosService: EOSService,
+ private readonly patch: PatchDbService,
) {}
ngOnInit() {
- this.subs = [
- this.patch
- .watch$('server-info', 'status-info', 'backing-up')
- .pipe()
- .subscribe(isBackingUp => {
- if (isBackingUp) {
- if (!this.backingUp) {
- this.backingUp = true
- this.subscribeToBackup()
- }
- } else {
- if (this.backingUp) {
- this.backingUp = false
- this.pkgs.forEach(pkg => pkg.sub?.unsubscribe())
- this.navCtrl.navigateRoot('/embassy')
- }
- }
- }),
- ]
+ this.backingUp$
+ .pipe(skip(1), takeUntil(this.destroy$))
+ .subscribe(isBackingUp => {
+ if (!isBackingUp) {
+ this.navCtrl.navigateRoot('/embassy')
+ }
+ })
}
- ngOnDestroy() {
- this.subs.forEach(sub => sub.unsubscribe())
- this.pkgs.forEach(pkg => pkg.sub?.unsubscribe())
- }
-
- async presentModalPassword(
+ async presentModalSelect(
target: MappedBackupTarget,
- ): Promise {
- let message =
- 'Enter your master password to create an encrypted backup of your Embassy and all its services.'
- if (!target.hasValidBackup) {
- message =
- message +
- ' Since this is a fresh backup, it could take a while. Future backups will likely be much faster.'
- }
+ ) {
+ this.target = target
+ const modal = await this.modalCtrl.create({
+ presentingElement: await this.modalCtrl.getTop(),
+ component: BackupSelectPage,
+ })
+
+ modal.onWillDismiss().then(res => {
+ if (res.data) {
+ this.serviceIds = res.data
+ this.presentModalPassword()
+ }
+ })
+
+ await modal.present()
+ }
+
+ async presentModalPassword(): Promise {
const options: GenericInputOptions = {
title: 'Master Password Needed',
- message,
+ message: 'Enter your master password to encrypt this backup.',
label: 'Master Password',
placeholder: 'Enter master password',
useMask: true,
@@ -93,23 +88,20 @@ export class ServerBackupPage {
argon2.verify(passwordHash, password)
// first time backup
- if (!target.hasValidBackup) {
- await this.createBackup(target.id, password)
+ if (!this.target.hasValidBackup) {
+ await this.createBackup(password)
// existing backup
} else {
try {
const passwordHash =
- target.entry['embassy-os']?.['password-hash'] || ''
+ this.target.entry['embassy-os']?.['password-hash'] || ''
argon2.verify(passwordHash, password)
} catch {
- setTimeout(
- () => this.presentModalOldPassword(target, password),
- 500,
- )
+ setTimeout(() => this.presentModalOldPassword(password), 500)
return
}
- await this.createBackup(target.id, password)
+ await this.createBackup(password)
}
},
}
@@ -123,10 +115,7 @@ export class ServerBackupPage {
await m.present()
}
- private async presentModalOldPassword(
- target: MappedBackupTarget,
- password: string,
- ): Promise {
+ private async presentModalOldPassword(password: string): Promise {
const options: GenericInputOptions = {
title: 'Original Password Needed',
message:
@@ -136,10 +125,11 @@ export class ServerBackupPage {
useMask: true,
buttonText: 'Create Backup',
submitFn: async (oldPassword: string) => {
- const passwordHash = target.entry['embassy-os']?.['password-hash'] || ''
+ const passwordHash =
+ this.target.entry['embassy-os']?.['password-hash'] || ''
argon2.verify(passwordHash, oldPassword)
- await this.createBackup(target.id, password, oldPassword)
+ await this.createBackup(password, oldPassword)
},
}
@@ -153,7 +143,6 @@ export class ServerBackupPage {
}
private async createBackup(
- id: string,
password: string,
oldPassword?: string,
): Promise {
@@ -164,7 +153,8 @@ export class ServerBackupPage {
try {
await this.embassyApi.createBackup({
- 'target-id': id,
+ 'target-id': this.target.id,
+ 'package-ids': this.serviceIds,
'old-password': oldPassword || null,
password,
})
@@ -172,53 +162,4 @@ export class ServerBackupPage {
loader.dismiss()
}
}
-
- private subscribeToBackup() {
- this.patch
- .watch$('package-data')
- .pipe(take(1))
- .subscribe(pkgs => {
- const pkgArr = Object.keys(pkgs)
- .sort()
- .map(key => pkgs[key])
- const activeIndex = pkgArr.findIndex(
- pkg =>
- pkg.installed?.status.main.status === PackageMainStatus.BackingUp,
- )
-
- this.pkgs = pkgArr.map((pkg, i) => ({
- entry: pkg,
- active: i === activeIndex,
- complete: i < activeIndex,
- }))
-
- // subscribe to pkg
- this.pkgs.forEach(pkg => {
- pkg.sub = this.patch
- .watch$(
- 'package-data',
- pkg.entry.manifest.id,
- 'installed',
- 'status',
- 'main',
- 'status',
- )
- .subscribe(status => {
- if (status === PackageMainStatus.BackingUp) {
- pkg.active = true
- } else if (pkg.active) {
- pkg.active = false
- pkg.complete = true
- }
- })
- })
- })
- }
-}
-
-interface PkgInfo {
- entry: PackageDataEntry
- active: boolean
- complete: boolean
- sub?: Subscription
}
diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html
index 7ddbc3f36..2468a1507 100644
--- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html
+++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html
@@ -52,17 +52,17 @@
Last Backup: {{ server['last-backup'] ?
(server['last-backup'] | date: 'short') : 'never' }}
-
+
- Backing up
+ Backing up
diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts
index 6dba316b7..703301aa8 100644
--- a/frontend/projects/ui/src/app/services/api/api.fixures.ts
+++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts
@@ -20,7 +20,7 @@ import { MarketplacePkg } from '@start9labs/marketplace'
export module Mock {
export const ServerUpdated: ServerStatusInfo = {
- 'backing-up': false,
+ 'backup-progress': null,
'update-progress': null,
updated: true,
}
diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts
index b1f8a45e1..0166dfc9a 100644
--- a/frontend/projects/ui/src/app/services/api/api.types.ts
+++ b/frontend/projects/ui/src/app/services/api/api.types.ts
@@ -148,6 +148,7 @@ export module RR {
export type CreateBackupReq = WithExpire<{
// backup.create
'target-id': string
+ 'package-ids': string[]
'old-password': string | null
password: string
}>
diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
index 0c859a621..99173f9bf 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
@@ -392,12 +392,13 @@ export class MockApiService extends ApiService {
params: RR.CreateBackupReq,
): Promise {
await pauseFor(2000)
- const path = '/server-info/status-info/backing-up'
- const ids = ['bitcoind', 'lnd']
+ const path = '/server-info/status-info/backup-progress'
+ const ids = params['package-ids']
setTimeout(async () => {
for (let i = 0; i < ids.length; i++) {
- const appPath = `/package-data/${ids[i]}/installed/status/main/status`
+ const id = ids[i]
+ const appPath = `/package-data/${id}/installed/status/main/status`
const appPatch = [
{
op: PatchOp.REPLACE,
@@ -409,13 +410,19 @@ export class MockApiService extends ApiService {
await pauseFor(8000)
- const newPatch = [
+ this.updateMock([
{
...appPatch[0],
value: PackageMainStatus.Stopped,
},
- ]
- this.updateMock(newPatch)
+ ])
+ this.updateMock([
+ {
+ op: PatchOp.REPLACE,
+ path: `${path}/${id}/complete`,
+ value: true,
+ },
+ ])
}
await pauseFor(1000)
@@ -425,7 +432,7 @@ export class MockApiService extends ApiService {
{
op: PatchOp.REPLACE,
path,
- value: false,
+ value: null,
},
]
this.updateMock(lastPatch)
@@ -435,7 +442,12 @@ export class MockApiService extends ApiService {
{
op: PatchOp.REPLACE,
path,
- value: true,
+ value: ids.reduce((acc, val) => {
+ return {
+ ...acc,
+ [val]: { complete: false },
+ }
+ }, {}),
},
]
diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts
index 26d3a0763..e1954f749 100644
--- a/frontend/projects/ui/src/app/services/api/mock-patch.ts
+++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts
@@ -27,7 +27,7 @@ export const mockPatchData: DataModel = {
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'eos-version-compat': '>=0.3.0 <=0.3.0.1',
'status-info': {
- 'backing-up': false,
+ 'backup-progress': null,
updated: false,
'update-progress': null,
},
diff --git a/frontend/projects/ui/src/app/services/eos.service.ts b/frontend/projects/ui/src/app/services/eos.service.ts
index b429145f6..a1907291d 100644
--- a/frontend/projects/ui/src/app/services/eos.service.ts
+++ b/frontend/projects/ui/src/app/services/eos.service.ts
@@ -4,7 +4,7 @@ import { MarketplaceEOS } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Emver } from '@start9labs/shared'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
-import { map } from 'rxjs/operators'
+import { distinctUntilChanged, map } from 'rxjs/operators'
@Injectable({
providedIn: 'root',
@@ -15,15 +15,17 @@ export class EOSService {
readonly updating$ = this.patch.watch$('server-info', 'status-info').pipe(
map(status => {
- return status && (!!status['update-progress'] || status.updated)
+ return !!status['update-progress'] || status.updated
}),
+ distinctUntilChanged(),
)
- readonly backingUp$ = this.patch.watch$(
- 'server-info',
- 'status-info',
- 'backing-up',
- )
+ readonly backingUp$ = this.patch
+ .watch$('server-info', 'status-info', 'backup-progress')
+ .pipe(
+ map(obj => !!obj),
+ distinctUntilChanged(),
+ )
readonly updatingOrBackingUp$ = combineLatest([
this.updating$,
diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts
index 367b9c40f..c3a9013bb 100644
--- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts
+++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts
@@ -59,7 +59,11 @@ export interface ServerInfo {
}
export interface ServerStatusInfo {
- 'backing-up': boolean
+ 'backup-progress': null | {
+ [packageId: string]: {
+ complete: boolean
+ }
+ }
updated: boolean
'update-progress': { size: number | null; downloaded: number } | null
}