feat: move all frontend projects under the same Angular workspace (#1141)

* feat: move all frontend projects under the same Angular workspace

* Refactor/angular workspace (#1154)

* update frontend build steps

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2022-01-31 14:01:33 -07:00
committed by GitHub
parent 7e6c852ebd
commit 574539faec
504 changed files with 11569 additions and 78972 deletions

View File

@@ -0,0 +1,14 @@
<div class="inline">
<!-- has backup -->
<h2 *ngIf="hasValidBackup; else noBackup">
<ion-icon name="cloud-done" color="success"></ion-icon>
{{ is02x ? 'Embassy 0.2.x detected' : 'Embassy backup detected' }}
</h2>
<!-- no backup -->
<ng-template #noBackup>
<h2>
<ion-icon name="cloud-offline" color="danger"></ion-icon>
No Embassy backup
</h2>
</ng-template>
</div>

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { RecoverPage } from './recover.page'
const routes: Routes = [
{
path: '',
component: RecoverPage,
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class RecoverPageRoutingModule { }

View File

@@ -0,0 +1,25 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { DriveStatusComponent, RecoverPage } from './recover.page'
import { PasswordPageModule } from '../../modals/password/password.module'
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
import { RecoverPageRoutingModule } from './recover-routing.module'
import { PipesModule } from 'src/app/pipes/pipe.module'
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
@NgModule({
declarations: [RecoverPage, DriveStatusComponent],
imports: [
CommonModule,
FormsModule,
IonicModule,
RecoverPageRoutingModule,
PasswordPageModule,
ProdKeyModalModule,
PipesModule,
CifsModalModule,
],
})
export class RecoverPageModule { }

View File

@@ -0,0 +1,65 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col>
<div style="padding-bottom: 32px;" class="ion-text-center">
<img src="assets/img/logo.png" style="max-width: 240px;" />
</div>
<ion-card color="dark">
<ion-card-header class="ion-text-center">
<ion-card-title>Recover</ion-card-title>
<ion-card-subtitle>Select the shared folder or drive containing your Embassy backup</ion-card-subtitle>
</ion-card-header>
<ion-card-content class="ion-margin">
<ion-spinner *ngIf="loading" class="center-spinner" name="lines"></ion-spinner>
<!-- loaded -->
<ion-item-group *ngIf="!loading">
<!-- cifs -->
<h2 class="target-label">
Shared Network Folder
</h2>
<p class="ion-padding-bottom">
Using a shared folder is the recommended way to recover from backup, since it works with all Embassy hardware configurations.
To recover from a shared folder, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#shared-network-folder" target="_blank" noreferrer>instructions</a>.
</p>
<!-- connect -->
<ion-item button lines="none" (click)="presentModalCifs()">
<ion-icon slot="start" name="folder-open-outline" size="large" color="light"></ion-icon>
<ion-label>Open Shared Folder</ion-label>
</ion-item>
<br />
<br />
<!-- drives -->
<h2 class="target-label">
Physical Drives
</h2>
<p class="ion-padding-bottom">
Warning! Plugging in more than one physical drive to Embassy can lead to power failure and data corruption.
To recover from a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#physical-drive" target="_blank" noreferrer>instructions</a>.
</p>
<ng-container *ngFor="let mapped of mappedDrives">
<ion-item button *ngIf="mapped.drive as drive" [disabled]="!driveClickable(mapped)" (click)="select(drive)">
<ion-icon slot="start" name="save-outline" size="large" color="light"></ion-icon>
<ion-label>
<h1>{{ drive.label || drive.logicalname }}</h1>
<drive-status [hasValidBackup]="mapped.hasValidBackup" [is02x]="mapped.is02x"></drive-status>
<p>{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }}</p>
<p>Capacity: {{ drive.capacity | convertBytes }}</p>
</ion-label>
</ion-item>
</ng-container>
</ion-item-group>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,4 @@
.target-label {
font-weight: bold;
padding-bottom: 6px;
}

View File

@@ -0,0 +1,223 @@
import { Component, Input } from '@angular/core'
import { AlertController, IonicSafeString, LoadingController, ModalController, NavController } from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
@Component({
selector: 'app-recover',
templateUrl: 'recover.page.html',
styleUrls: ['recover.page.scss'],
})
export class RecoverPage {
loading = true
mappedDrives: MappedDisk[] = []
hasShownGuidAlert = false
constructor (
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private readonly modalCtrl: ModalController,
private readonly modalController: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService,
public readonly stateService: StateService,
) { }
async ngOnInit () {
await this.getDrives()
}
async refresh () {
this.loading = true
await this.getDrives()
}
driveClickable (mapped: MappedDisk) {
return mapped.drive['embassy-os']?.full && (this.stateService.hasProductKey || mapped.is02x)
}
async getDrives () {
this.mappedDrives = []
try {
const { disks, reconnect } = await this.apiService.getDrives()
disks.filter(d => d.partitions.length).forEach(d => {
d.partitions.forEach(p => {
const drive: DiskBackupTarget = {
vendor: d.vendor,
model: d.model,
logicalname: p.logicalname,
label: p.label,
capacity: p.capacity,
used: p.used,
'embassy-os': p['embassy-os'],
}
this.mappedDrives.push(
{
hasValidBackup: p['embassy-os']?.full,
is02x: drive['embassy-os']?.version.startsWith('0.2'),
drive,
},
)
})
})
if (!this.mappedDrives.length && reconnect.length) {
const list = `<ul>${reconnect.map(recon => `<li>${recon}</li>`)}</ul>`
const alert = await this.alertCtrl.create({
header: 'Warning',
message: `One or more devices you connected had to be reconfigured to support the current hardware platform. Please unplug and replug the following device(s), then refresh the page:<br> ${list}`,
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
})
await alert.present()
}
const importableDrive = disks.find(d => !!d.guid)
if (!!importableDrive && !this.hasShownGuidAlert) {
const alert = await this.alertCtrl.create({
header: 'Embassy Data Drive Detected',
message: new IonicSafeString(`${importableDrive.vendor || 'Unknown Vendor'} - ${importableDrive.model || 'Unknown Model' } contains Embassy data. To use this drive and its data <i>as-is</i>, click "Use Drive". This will complete the setup process.<br /><br /><b>Important</b>. If you are trying to restore from backup or update from 0.2.x, DO NOT click "Use Drive". Instead, click "Cancel" and follow instructions.`),
buttons: [
{
role: 'cancel',
text: 'Cancel',
},
{
text: 'Use Drive',
handler: async () => {
await this.importDrive(importableDrive.guid)
},
},
],
})
await alert.present()
this.hasShownGuidAlert = true
}
} catch (e) {
this.errorToastService.present(`${e.message}: ${e.details}`)
} finally {
this.loading = false
}
}
async presentModalCifs (): Promise<void> {
const modal = await this.modalCtrl.create({
component: CifsModal,
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
const { hostname, path, username, password } = res.data.cifs
this.stateService.recoverySource = {
type: 'cifs',
hostname,
path,
username,
password,
}
this.stateService.recoveryPassword = res.data.recoveryPassword
this.navCtrl.navigateForward('/embassy')
}
})
await modal.present()
}
async select (target: DiskBackupTarget) {
const is02x = target['embassy-os'].version.startsWith('0.2')
if (this.stateService.hasProductKey) {
if (is02x) {
this.selectRecoverySource(target.logicalname)
} else {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.selectRecoverySource(target.logicalname, res.data.password)
}
})
await modal.present()
}
// if no product key, it means they are an upgrade kit user
} else {
if (!is02x) {
const alert = await this.alertCtrl.create({
header: 'Error',
message: 'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
buttons: [
{
role: 'cancel',
text: 'OK',
},
],
})
await alert.present()
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.productKey) {
this.selectRecoverySource(target.logicalname)
}
})
await modal.present()
}
}
}
private async importDrive (guid: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(guid)
await this.navCtrl.navigateForward(`/init`)
} catch (e) {
this.errorToastService.present(`${e.message}: ${e.details}`)
} finally {
loader.dismiss()
}
}
private async selectRecoverySource (logicalname: string, password?: string) {
this.stateService.recoverySource = {
type: 'disk',
logicalname,
}
this.stateService.recoveryPassword = password
this.navCtrl.navigateForward(`/embassy`)
}
}
@Component({
selector: 'drive-status',
templateUrl: './drive-status.component.html',
styleUrls: ['./recover.page.scss'],
})
export class DriveStatusComponent {
@Input() hasValidBackup: boolean
@Input() is02x: boolean
}
interface MappedDisk {
is02x: boolean
hasValidBackup: boolean
drive: DiskBackupTarget
}