mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
0.3.0 refactor
ui: adds overlay layer to patch-db-client ui: getting towards mocks ui: cleans up factory init ui: nice type hack ui: live api for patch ui: api service source + http starts up ui: api source + http ui: rework patchdb config, pass stashTimeout into patchDbModel wires in temp patching into api service ui: example of wiring patchdbmodel into page begin integration remove unnecessary method linting first data rendering rework app initialization http source working for ssh delete call temp patches working entire Embassy tab complete not in kansas anymore ripping, saving progress progress for API request response types and endoint defs Update data-model.ts shambles, but in a good way progress big progress progress installed list working big progress progress progress begin marketplace redesign Update api-types.ts Update api-types.ts marketplace improvements cosmetic dependencies and recommendations begin nym auth approach install wizard restore flow and donations
This commit is contained in:
committed by
Aiden McClelland
parent
fd685ae32c
commit
594d93eb3b
@@ -1,17 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppBackupPage } from './app-backup.page'
|
||||
import { AppBackupConfirmationComponentModule } from 'src/app/components/app-backup-confirmation/app-backup-confirmation.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppBackupPage],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
AppBackupConfirmationComponentModule,
|
||||
],
|
||||
entryComponents: [AppBackupPage],
|
||||
exports: [AppBackupPage],
|
||||
})
|
||||
export class AppBackupPageModule { }
|
||||
@@ -1,61 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ type === 'create' ? 'Create Backup' : 'Restore Backup' }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="presentAlertHelp()" color="primary">
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-spinner *ngIf="loading" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<ion-item *ngIf="type === 'restore' && (app.restoreAlert || defaultRestoreAlert) as restore" class="notifier-item" style="box-shadow: 0 0 5px 1px var(--ion-color-danger); margin-bottom: 40px">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
||||
<ion-icon style="margin-right: 5px;" slot="start" color="danger" slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-text color="danger" style="font-size: medium; font-weight: bold">Warning</ion-text>
|
||||
</h2>
|
||||
<p style="font-size: small">{{restore}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="allPartitionsMounted">
|
||||
<ion-text *ngIf="type === 'create'" class="ion-text-wrap" color="warning">No partitions available. To begin a backup, insert a storage device into your Embassy.</ion-text>
|
||||
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you wish to restore.</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group *ngFor="let d of disks">
|
||||
<ion-item-divider>{{ d.logicalname }} ({{ d.size }})</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item button [disabled]="p.isMounted" *ngFor="let p of d.partitions" (click)="presentAlert(d, p)">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ p.label || p.logicalname }}</h2>
|
||||
<p>{{ p.size || 'unknown size' }}</p>
|
||||
<p *ngIf="!p.isMounted"><ion-text color="success">Available</ion-text></p>
|
||||
<p *ngIf="p.isMounted"><ion-text color="danger">Unvailable</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
.toast-close-button {
|
||||
color: var(--ion-color-primary) !important;
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, AlertController, LoadingController, ToastController } from '@ionic/angular'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { DiskInfo, DiskPartition } from 'src/app/models/server-model'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { concatMap } from 'rxjs/operators'
|
||||
import { AppBackupConfirmationComponent } from 'src/app/components/app-backup-confirmation/app-backup-confirmation.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-backup',
|
||||
templateUrl: './app-backup.page.html',
|
||||
styleUrls: ['./app-backup.page.scss'],
|
||||
})
|
||||
export class AppBackupPage {
|
||||
@Input() app: AppInstalledFull
|
||||
@Input() type: 'create' | 'restore'
|
||||
disks: DiskInfo[]
|
||||
loading = true
|
||||
error: string
|
||||
allPartitionsMounted: boolean
|
||||
defaultRestoreAlert: string
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.defaultRestoreAlert = `Restoring ${this.app.title} will overwrite its current data.`
|
||||
return this.getExternalDisks().then(() => this.loading = false)
|
||||
}
|
||||
|
||||
async getExternalDisks (): Promise<void> {
|
||||
try {
|
||||
this.disks = await this.apiService.getExternalDisks()
|
||||
this.allPartitionsMounted = this.disks.every(d => d.partitions.every(p => p.isMounted))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.getExternalDisks(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
await this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
async presentAlertHelp (): Promise<void> {
|
||||
let alert: HTMLIonAlertElement
|
||||
if (this.type === 'create') {
|
||||
alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: `Backups`,
|
||||
message: `Select a location to back up ${this.app.title}.<br /><br />Internal drives and drives currently backing up other services will not be available.<br /><br />Depending on the amount of data in ${this.app.title}, your first backup may take a while. Since backups are diff-based, the speed of future backups to the same disk will likely be much faster.`,
|
||||
buttons: ['Dismiss'],
|
||||
})
|
||||
} else if (this.type === 'restore') {
|
||||
alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: `Backups`,
|
||||
message: `Select a location containing the backup you wish to restore for ${this.app.title}.<br /><br />Restoring ${this.app.title} will re-sync your service with your previous backup. The speed of the restore process depends on the backup size.`,
|
||||
buttons: ['Dismiss'],
|
||||
})
|
||||
}
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlert (disk: DiskInfo, partition: DiskPartition): Promise<void> {
|
||||
if (this.type === 'create') {
|
||||
this.presentAlertCreateEncrypted(disk, partition)
|
||||
} else {
|
||||
this.presentAlertWarn(partition)
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertCreateEncrypted (disk: DiskInfo, partition: DiskPartition): Promise<void> {
|
||||
const m = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
app: this.app,
|
||||
partition,
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
component: AppBackupConfirmationComponent,
|
||||
backdropDismiss: false,
|
||||
})
|
||||
|
||||
m.onWillDismiss().then(res => {
|
||||
const data = res.data
|
||||
if (data.cancel) return
|
||||
// TODO: EJECT-DISKS we hard code the 'eject' last argument to be false, until ejection is an option in the UI. When it is, add it to the data object above ^
|
||||
return this.create(disk, partition, data.password, false)
|
||||
})
|
||||
|
||||
return await m.present()
|
||||
}
|
||||
|
||||
private async presentAlertWarn (partition: DiskPartition): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: `Warning`,
|
||||
message: `Restoring ${this.app.title} from "${partition.label || partition.logicalname}" will overwrite its current data.<br /><br />Are you sure you want to continue?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
}, {
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
this.presentAlertRestore(partition)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async presentAlertRestore (partition: DiskPartition): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: `Decrypt Backup`,
|
||||
message: `Enter your master password`,
|
||||
inputs: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
placeholder: 'Password',
|
||||
},
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
}, {
|
||||
text: 'Restore',
|
||||
handler: (data) => {
|
||||
this.restore(partition, data.password)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async restore (partition: DiskPartition, password?: string): Promise<void> {
|
||||
this.error = ''
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader-ontop-of-all',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.restoreAppBackup(this.app.id, partition.logicalname, password)
|
||||
this.appModel.update({ id: this.app.id, status: AppStatus.RESTORING_BACKUP })
|
||||
await this.dismiss()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async create (disk: DiskInfo, partition: DiskPartition, password: string, eject: boolean): Promise<void> {
|
||||
this.error = ''
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader-ontop-of-all',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.createAppBackup(this.app.id, partition.logicalname, password)
|
||||
this.appModel.update({ id: this.app.id, status: AppStatus.CREATING_BACKUP })
|
||||
if (eject) {
|
||||
this.appModel.watchForBackup(this.app.id).pipe(concatMap(
|
||||
() => this.apiService.ejectExternalDisk(disk.logicalname),
|
||||
)).subscribe({
|
||||
next: () => this.toastEjection(disk, true),
|
||||
error: () => this.toastEjection(disk, false),
|
||||
})
|
||||
}
|
||||
await this.dismiss()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async toastEjection (disk: DiskInfo, success: boolean) {
|
||||
const { header, message, cssClass } = success ? {
|
||||
header: 'Success',
|
||||
message: `Drive ${disk.logicalname} ejected successfully`,
|
||||
cssClass: 'notification-toast',
|
||||
} : {
|
||||
header: 'Error',
|
||||
message: `Drive ${disk.logicalname} did not eject successfully`,
|
||||
cssClass: 'alert-error-message',
|
||||
}
|
||||
const t = await this.toastCtrl.create({
|
||||
header,
|
||||
message,
|
||||
cssClass,
|
||||
duration: 2000,
|
||||
})
|
||||
await t.present()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Type } from '@angular/core'
|
||||
import { ValueType } from 'src/app/app-config/config-types'
|
||||
import { ValueType } from 'src/app/pkg-config/config-types'
|
||||
|
||||
export type AppConfigComponentMapping = { [k in ValueType]: Type<any> }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { Annotations, Range } from '../../app-config/config-utilities'
|
||||
import { Annotations, Range } from '../../pkg-config/config-utilities'
|
||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
||||
import { ValueSpecList, isValueSpecListOf } from 'src/app/app-config/config-types'
|
||||
import { ModalPresentable } from 'src/app/app-config/modal-presentable'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ValueSpecList, isValueSpecListOf } from 'src/app/pkg-config/config-types'
|
||||
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config-list',
|
||||
@@ -106,7 +106,7 @@ export class AppConfigListPage extends ModalPresentable {
|
||||
|
||||
async presentAlertDelete (key: number, e: Event) {
|
||||
e.stopPropagation()
|
||||
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Caution',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, AlertController } from '@ionic/angular'
|
||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
||||
import { ValueSpecObject } from 'src/app/app-config/config-types'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config-object',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
||||
import { ValueSpecUnion } from 'src/app/app-config/config-types'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ValueSpecUnion } from 'src/app/pkg-config/config-types'
|
||||
import { ObjectConfigComponent } from 'src/app/components/object-config/object-config.component'
|
||||
import { mapUnionSpec } from '../../app-config/config-utilities'
|
||||
import { mapUnionSpec } from '../../pkg-config/config-utilities'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config-union',
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<p *ngIf="rangeDescription">
|
||||
<ion-text color="medium">{{ rangeDescription }}</ion-text>
|
||||
</p>
|
||||
<p *ngIf="spec.default">
|
||||
<p *ngIf="spec.default !== undefined">
|
||||
<ion-text color="medium">
|
||||
<p>Default: {{ defaultDescription }} <ion-icon style="padding-left: 8px;" name="refresh-outline" color="primary" (click)="refreshDefault()"></ion-icon></p>
|
||||
<p *ngIf="spec.type === 'number' && spec.units">Units: {{ spec.units }}</p>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/app-config/config-utilities'
|
||||
import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/pkg-config/config-utilities'
|
||||
import { AlertController, ToastController } from '@ionic/angular'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
||||
import { ValueSpecOf } from 'src/app/app-config/config-types'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ValueSpecOf } from 'src/app/pkg-config/config-types'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!-- TODO: EJECT-DISKS, add a check box to allow a user to eject a disk on backup completion. -->
|
||||
<ion-content>
|
||||
<div style="height: 85%; margin: 20px; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div>
|
||||
<h4><ion-text color="dark">Ready to Backup</ion-text></h4>
|
||||
<p><ion-text color="medium">Enter your master password to create an encrypted backup.</ion-text></p>
|
||||
</div>
|
||||
<div>
|
||||
<ion-item lines="none" style="--background: var(--ion-background-color); --border-color: var(--ion-color-medium);">
|
||||
<ion-label style="font-size: small" position="floating">Master Password</ion-label>
|
||||
<ion-input style="border-style: solid; border-width: 0px 0px 1px 0px; border-color: var(--ion-color-dark);" [(ngModel)]="password" type="password" (ionChange)="error = ''"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="error" lines="none" style="--background: var(--ion-background-color);">
|
||||
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{ error }}</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; align-items: center;">
|
||||
<ion-button fill="clear" color="medium" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="primary" (click)="submit()">
|
||||
Create Backup
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { BackupConfirmationComponent } from './backup-confirmation.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BackupConfirmationComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
FormsModule,
|
||||
],
|
||||
exports: [BackupConfirmationComponent],
|
||||
})
|
||||
export class BackupConfirmationComponentModule { }
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { PartitionInfo } from 'src/app/services/api/api-types'
|
||||
|
||||
@Component({
|
||||
selector: 'backup-confirmation',
|
||||
templateUrl: './backup-confirmation.component.html',
|
||||
styleUrls: ['./backup-confirmation.component.scss'],
|
||||
})
|
||||
export class BackupConfirmationComponent {
|
||||
@Input() name: string
|
||||
unmasked = false
|
||||
password: string
|
||||
message: string
|
||||
error = ''
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.message = `Enter your master password to create an encrypted backup on "${this.name}".`
|
||||
}
|
||||
|
||||
toggleMask () {
|
||||
this.unmasked = !this.unmasked
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalCtrl.dismiss({ cancel: true })
|
||||
}
|
||||
|
||||
submit () {
|
||||
if (!this.password || this.password.length < 12) {
|
||||
this.error = 'Password must be at least 12 characters in length.'
|
||||
return
|
||||
}
|
||||
const { password } = this
|
||||
this.modalCtrl.dismiss({ password })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { ServerModel } from 'src/app/models/server-model'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@@ -19,7 +18,7 @@ export class OSWelcomePage {
|
||||
) { }
|
||||
|
||||
async dismiss () {
|
||||
this.apiService.acknowledgeOSWelcome(this.config.version).catch(console.error)
|
||||
this.apiService.setDbValue({ pointer: '/welcome-ack', value: this.config.version }).catch(console.error)
|
||||
|
||||
// return false to skip subsequent alert modals (e.g. check for updates modals)
|
||||
// return true to show subsequent alert modals
|
||||
|
||||
Reference in New Issue
Block a user