display bottom item in backup list and refactor for cleanliness (#1609)

* display bottom item in backup list and refactor for cleanliness

* fix spelling mistake

* display initial toggle to deselect all, as all are selected by default

* add select/deselect all to backup restore and handle backup case when no services intalled

Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Matt Hill
2022-07-04 14:16:18 -06:00
committed by GitHub
parent 6d805ae941
commit 9319314672
11 changed files with 214 additions and 181 deletions

View File

@@ -3,7 +3,9 @@
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-back-button defaultHref="embassy"></ion-back-button> <ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>{{ title }}</ion-title> <ion-title>{{
type === 'create' ? 'Create Backup' : 'Restore From Backup'
}}</ion-title>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button [disabled]="backupService.loading" (click)="refresh()"> <ion-button [disabled]="backupService.loading" (click)="refresh()">
Refresh Refresh
@@ -11,4 +13,4 @@
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>

View File

@@ -1,137 +1,145 @@
<!-- loading --> <backup-drives-header [type]="type"></backup-drives-header>
<text-spinner
*ngIf="backupService.loading; else loaded"
[text]="loadingText"
></text-spinner>
<!-- loaded --> <ion-content class="ion-padding">
<ng-template #loaded> <!-- loading -->
<!-- error --> <text-spinner
<ion-item *ngIf="backupService.loadingError; else noError"> *ngIf="backupService.loading; else loaded"
<ion-label> [text]="loadingText"
<ion-text color="danger"> ></text-spinner>
{{ backupService.loadingError }}
</ion-text>
</ion-label>
</ion-item>
<ng-template #noError> <!-- loaded -->
<ion-item-group> <ng-template #loaded>
<!-- ** cifs ** --> <!-- error -->
<ion-item-divider>LAN Shared Folders</ion-item-divider> <ion-item *ngIf="backupService.loadingError; else noError">
<ion-item> <ion-label>
<ion-label> <ion-text color="danger">
<h2> {{ backupService.loadingError }}
LAN Shared Folders are the recommended way to create Embassy </ion-text>
backups. View the </ion-label>
<a </ion-item>
href="https://start9.com/latest/user-manual/backups/cifs-setup"
target="_blank" <ng-template #noError>
noreferrer <ion-item-group>
>Instructions</a <!-- ** cifs ** -->
> <ion-item-divider>LAN Shared Folders</ion-item-divider>
</h2> <ion-item>
</ion-label> <ion-label>
</ion-item> <h2>
<!-- add new cifs --> LAN Shared Folders are the recommended way to create Embassy
<ion-item button detail="false" (click)="presentModalAddCifs()"> backups. View the
<ion-icon <a
slot="start" href="https://start9.com/latest/user-manual/backups/cifs-setup"
name="folder-open-outline" target="_blank"
size="large" noreferrer
color="dark" >Instructions</a
></ion-icon> >
<ion-label> </h2>
<b>Open New</b> </ion-label>
</ion-label> </ion-item>
</ion-item> <!-- add new cifs -->
<!-- cifs list --> <ion-item button detail="false" (click)="presentModalAddCifs()">
<ng-container *ngFor="let target of backupService.cifs; let i = index">
<ion-item button *ngIf="target.entry as cifs" (click)="select(target)">
<ion-icon <ion-icon
slot="start" slot="start"
name="folder-open-outline" name="folder-open-outline"
size="large" size="large"
color="dark"
></ion-icon> ></ion-icon>
<ion-label> <ion-label>
<h1>{{ cifs.path.split('/').pop() }}</h1> <b>Open New</b>
<ng-container *ngIf="cifs.mountable">
<backup-drives-status
[type]="type"
[hasValidBackup]="target.hasValidBackup"
></backup-drives-status>
</ng-container>
<h2 *ngIf="!cifs.mountable" class="inline">
<ion-icon name="cellular-outline" color="danger"></ion-icon>
Unable to connect
</h2>
<p>Hostname: {{ cifs.hostname }}</p>
<p>Path: {{ cifs.path }}</p>
</ion-label> </ion-label>
<ion-note </ion-item>
slot="end" <!-- cifs list -->
class="click-area" <ng-container *ngFor="let target of backupService.cifs; let i = index">
(click)="presentActionCifs($event, target, i)" <ion-item
button
*ngIf="target.entry as cifs"
(click)="select(target)"
> >
<ion-icon name="ellipsis-horizontal"></ion-icon> <ion-icon
</ion-note> slot="start"
</ion-item> name="folder-open-outline"
</ng-container> size="large"
></ion-icon>
<br />
<!-- ** drives ** -->
<ion-item-divider>Physical Drives</ion-item-divider>
<!-- no drives -->
<ion-item
*ngIf="!backupService.drives.length; else hasDrives"
class="ion-padding-bottom"
>
<ion-label>
<h2>
<ion-text color="warning">
Warning! Plugging a 2nd physical drive directly into your Embassy
can lead to data corruption.
</ion-text>
To safely create a backup to a physical drive, view the
<a
href="https://start9.com/latest/user-manual/backups/backups-create/#backup-using-a-physical-drive"
target="_blank"
noreferrer
>instructions</a
>.
</h2>
<br />
<h2>
If your drive is plugged in and does not appear, try
<a (click)="refresh()" style="cursor: pointer">refreshing</a>.
</h2>
</ion-label>
</ion-item>
<!-- drives detected -->
<ng-template #hasDrives>
<ion-item
button
*ngFor="let target of backupService.drives"
(click)="select(target)"
>
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
<ng-container *ngIf="target.entry as drive">
<ion-label> <ion-label>
<h1>{{ drive.label || drive.logicalname }}</h1> <h1>{{ cifs.path.split('/').pop() }}</h1>
<backup-drives-status <ng-container *ngIf="cifs.mountable">
[type]="type" <backup-drives-status
[hasValidBackup]="target.hasValidBackup" [type]="type"
></backup-drives-status> [hasValidBackup]="target.hasValidBackup"
<p> ></backup-drives-status>
{{ drive.vendor || 'Unknown Vendor' }} - </ng-container>
{{ drive.model || 'Unknown Model' }} <h2 *ngIf="!cifs.mountable" class="inline">
</p> <ion-icon name="cellular-outline" color="danger"></ion-icon>
<p>Capacity: {{ drive.capacity | convertBytes }}</p> Unable to connect
</h2>
<p>Hostname: {{ cifs.hostname }}</p>
<p>Path: {{ cifs.path }}</p>
</ion-label> </ion-label>
</ng-container> <ion-note
slot="end"
class="click-area"
(click)="presentActionCifs($event, target, i)"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</ion-note>
</ion-item>
</ng-container>
<br />
<!-- ** drives ** -->
<ion-item-divider>Physical Drives</ion-item-divider>
<!-- no drives -->
<ion-item
*ngIf="!backupService.drives.length; else hasDrives"
class="ion-padding-bottom"
>
<ion-label>
<h2>
<ion-text color="warning">
Warning! Plugging a 2nd physical drive directly into your
Embassy can lead to data corruption.
</ion-text>
To safely create a backup to a physical drive, view the
<a
href="https://start9.com/latest/user-manual/backups/backups-create/#backup-using-a-physical-drive"
target="_blank"
noreferrer
>instructions</a
>.
</h2>
<br />
<h2>
If your drive is plugged in and does not appear, try
<a (click)="refresh()" style="cursor: pointer">refreshing</a>.
</h2>
</ion-label>
</ion-item> </ion-item>
</ng-template> <!-- drives detected -->
</ion-item-group> <ng-template #hasDrives>
<ion-item
button
*ngFor="let target of backupService.drives"
(click)="select(target)"
>
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
<ng-container *ngIf="target.entry as drive">
<ion-label>
<h1>{{ drive.label || drive.logicalname }}</h1>
<backup-drives-status
[type]="type"
[hasValidBackup]="target.hasValidBackup"
></backup-drives-status>
<p>
{{ drive.vendor || 'Unknown Vendor' }} -
{{ drive.model || 'Unknown Model' }}
</p>
<p>Capacity: {{ drive.capacity | convertBytes }}</p>
</ion-label>
</ng-container>
</ion-item>
</ng-template>
</ion-item-group>
</ng-template>
</ng-template> </ng-template>
</ng-template> </ion-content>

View File

@@ -17,13 +17,15 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
type BackupType = 'create' | 'restore'
@Component({ @Component({
selector: 'backup-drives', selector: 'backup-drives',
templateUrl: './backup-drives.component.html', templateUrl: './backup-drives.component.html',
styleUrls: ['./backup-drives.component.scss'], styleUrls: ['./backup-drives.component.scss'],
}) })
export class BackupDrivesComponent { export class BackupDrivesComponent {
@Input() type: 'create' | 'restore' @Input() type: BackupType
@Output() onSelect: EventEmitter< @Output() onSelect: EventEmitter<
MappedBackupTarget<CifsBackupTarget | DiskBackupTarget> MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>
> = new EventEmitter() > = new EventEmitter()
@@ -232,7 +234,7 @@ export class BackupDrivesComponent {
styleUrls: ['./backup-drives.component.scss'], styleUrls: ['./backup-drives.component.scss'],
}) })
export class BackupDrivesHeaderComponent { export class BackupDrivesHeaderComponent {
@Input() title: string @Input() type: BackupType
@Output() onClose: EventEmitter<void> = new EventEmitter() @Output() onClose: EventEmitter<void> = new EventEmitter()
constructor(public readonly backupService: BackupService) {} constructor(public readonly backupService: BackupService) {}

View File

@@ -11,6 +11,13 @@
<ion-content> <ion-content>
<ion-item-group> <ion-item-group>
<ion-item-divider>
<ion-buttons slot="end" style="padding-bottom: 6px">
<ion-button fill="clear" (click)="toggleSelectAll()">
<b>{{ selectAll ? 'Select All' : 'Deselect All' }}</b>
</ion-button>
</ion-buttons>
</ion-item-divider>
<ion-item *ngFor="let option of options"> <ion-item *ngFor="let option of options">
<ion-label> <ion-label>
<h2>{{ option.title }}</h2> <h2>{{ option.title }}</h2>

View File

@@ -27,6 +27,7 @@ export class AppRecoverSelectPage {
'newer-eos': boolean 'newer-eos': boolean
})[] })[]
hasSelection = false hasSelection = false
selectAll = true
error: string | IonicSafeString error: string | IonicSafeString
constructor( constructor(
@@ -62,6 +63,11 @@ export class AppRecoverSelectPage {
this.hasSelection = this.options.some(o => o.checked) this.hasSelection = this.options.some(o => o.checked)
} }
toggleSelectAll() {
this.options.forEach(pkg => (pkg.checked = this.selectAll))
this.selectAll = !this.selectAll
}
async restore(): Promise<void> { async restore(): Promise<void> {
const ids = this.options const ids = this.options
.filter(option => !!option.checked) .filter(option => !!option.checked)

View File

@@ -9,44 +9,50 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <div *ngIf="pkgs.length; else noServices">
<ion-item-group> <ion-content>
<ion-item-divider> <ion-item-group>
<ion-buttons slot="end" style="padding-bottom: 6px"> <ion-item-divider>
<ion-button fill="clear" (click)="toggleSelectAll()"> <ion-buttons slot="end" style="padding-bottom: 6px">
<b>{{ selectAll ? 'Select All' : 'Deselect All' }}</b> <ion-button fill="clear" (click)="toggleSelectAll()">
<b>{{ selectAll ? 'Select All' : 'Deselect All' }}</b>
</ion-button>
</ion-buttons>
</ion-item-divider>
<ion-item *ngFor="let pkg of pkgs">
<ion-avatar slot="start">
<img alt="" [src]="pkg.icon" />
</ion-avatar>
<ion-label>
<h2>{{ pkg.title }}</h2>
</ion-label>
<ion-checkbox
slot="end"
[(ngModel)]="pkg.checked"
(ionChange)="handleChange()"
[disabled]="pkg.disabled"
></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="solid"
color="primary"
(click)="dismiss(true)"
class="enter-click btn-128"
>
Back Up Selected
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
</ion-item-divider> </ion-toolbar>
<ion-item *ngFor="let pkg of pkgs"> </ion-footer>
<ion-avatar slot="start"> </div>
<img alt="" [src]="pkg.icon" />
</ion-avatar>
<ion-label>
<h2>{{ pkg.title }}</h2>
</ion-label>
<ion-checkbox
slot="end"
[(ngModel)]="pkg.checked"
(ionChange)="handleChange()"
[disabled]="pkg.disabled"
></ion-checkbox>
</ion-item>
</ion-item-group>
</ion-content>
<ion-footer> <ng-template #noServices>
<ion-toolbar> <ion-content><h2 class="center">No services installed!</h2></ion-content>
<ion-buttons slot="end" class="ion-padding-end"> </ng-template>
<ion-button
[disabled]="!hasSelection"
fill="solid"
color="primary"
(click)="dismiss(true)"
class="enter-click btn-128"
>
Back Up Selected
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,4 @@
.center {
text-align: center;
padding: 20px;
}

View File

@@ -11,7 +11,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
}) })
export class BackupSelectPage { export class BackupSelectPage {
hasSelection = false hasSelection = false
selectAll = true selectAll = false
pkgs: { pkgs: {
id: string id: string
title: string title: string

View File

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

View File

@@ -1,16 +1,14 @@
<!-- currently backing up --> <!-- currently backing up -->
<backing-up <backing-up
*ngIf="backingUp$ | async; else notBackingUp" *ngIf="backingUp$ | async; else notBackingUp"
style="height: 100%" class="ion-page"
></backing-up> ></backing-up>
<!-- not backing up --> <!-- not backing up -->
<ng-template #notBackingUp> <ng-template #notBackingUp>
<backup-drives-header title="Create Backup"></backup-drives-header> <backup-drives
<ion-content class="ion-padding"> type="create"
<backup-drives class="ion-page"
type="create" (onSelect)="presentModalSelect($event)"
(onSelect)="presentModalSelect($event)" ></backup-drives>
></backup-drives>
</ion-content>
</ng-template> </ng-template>

View File

@@ -78,7 +78,7 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
async updateServerWrapper(params: RR.UpdateServerReq) { async updateServerWrapper(params: RR.UpdateServerReq) {
const res = await this.updateServerRaw(params) const res = await this.updateServerRaw(params)
if (res.response === 'no-updates') { if (res.response === 'no-updates') {
throw new Error('Could ont find a newer version of EmbassyOS') throw new Error('Could not find a newer version of EmbassyOS')
} }
return res return res
} }
@@ -271,7 +271,7 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
private syncResponse< private syncResponse<
T, T,
F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>, F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>,
>(f: F, temp?: Operation<unknown>): (...args: Parameters<F>) => Promise<T> { >(f: F, temp?: Operation<unknown>): (...args: Parameters<F>) => Promise<T> {
return (...a) => { return (...a) => {
// let expireId = undefined // let expireId = undefined
// if (temp) { // if (temp) {