Feature/shared refactor (#1176)

* refactor: move most of the shared entities to @start8labs/shared library
This commit is contained in:
Alex Inkin
2022-02-15 18:13:05 +03:00
committed by GitHub
parent 1769f7e2e6
commit 7c26b18c73
164 changed files with 2908 additions and 2860 deletions

View File

@@ -1,8 +1,15 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { BackupDrivesComponent, BackupDrivesHeaderComponent, BackupDrivesStatusComponent } from './backup-drives.component'
import { SharingModule } from '../../modules/sharing.module'
import {
BackupDrivesComponent,
BackupDrivesHeaderComponent,
BackupDrivesStatusComponent,
} from './backup-drives.component'
import {
UnitConversionPipesModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
@NgModule({
@@ -14,7 +21,8 @@ import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.
imports: [
CommonModule,
IonicModule,
SharingModule,
UnitConversionPipesModule,
TextSpinnerComponentModule,
GenericFormPageModule,
],
exports: [
@@ -23,4 +31,4 @@ import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.
BackupDrivesStatusComponent,
],
})
export class BackupDrivesComponentModule { }
export class BackupDrivesComponentModule {}

View File

@@ -1,12 +1,21 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { BackupService } from './backup.service'
import { CifsBackupTarget, DiskBackupTarget, RR } from 'src/app/services/api/api.types'
import { ActionSheetController, AlertController, LoadingController, ModalController } from '@ionic/angular'
import {
CifsBackupTarget,
DiskBackupTarget,
RR,
} from 'src/app/services/api/api.types'
import {
ActionSheetController,
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { MappedBackupTarget } from 'src/app/util/misc.util'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
@Component({
selector: 'backup-drives',
@@ -15,10 +24,12 @@ import { MappedBackupTarget } from 'src/app/util/misc.util'
})
export class BackupDrivesComponent {
@Input() type: 'create' | 'restore'
@Output() onSelect: EventEmitter<MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>> = new EventEmitter()
@Output() onSelect: EventEmitter<
MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>
> = new EventEmitter()
loadingText: string
constructor (
constructor(
private readonly loadingCtrl: LoadingController,
private readonly actionCtrl: ActionSheetController,
private readonly alertCtrl: AlertController,
@@ -26,22 +37,30 @@ export class BackupDrivesComponent {
private readonly embassyApi: ApiService,
private readonly errToast: ErrorToastService,
public readonly backupService: BackupService,
) { }
) {}
ngOnInit () {
this.loadingText = this.type === 'create' ? 'Fetching Backup Targets' : 'Fetching Backup Sources'
ngOnInit() {
this.loadingText =
this.type === 'create'
? 'Fetching Backup Targets'
: 'Fetching Backup Sources'
this.backupService.getBackupTargets()
}
select (target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>): void {
select(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
): void {
if (target.entry.type === 'cifs' && !target.entry.mountable) {
const message = 'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.'
const message =
'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.'
this.presentAlertError(message)
return
}
if (this.type === 'restore' && !target.hasValidBackup) {
const message = `${target.entry.type === 'cifs' ? 'Shared folder' : 'Drive partition'} does not contain a valid Embassy backup.`
const message = `${
target.entry.type === 'cifs' ? 'Shared folder' : 'Drive partition'
} does not contain a valid Embassy backup.`
this.presentAlertError(message)
return
}
@@ -49,7 +68,7 @@ export class BackupDrivesComponent {
this.onSelect.emit(target)
}
async presentModalAddCifs (): Promise<void> {
async presentModalAddCifs(): Promise<void> {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
@@ -69,7 +88,10 @@ export class BackupDrivesComponent {
await modal.present()
}
async presentActionCifs (target: MappedBackupTarget<CifsBackupTarget>, index: number): Promise<void> {
async presentActionCifs(
target: MappedBackupTarget<CifsBackupTarget>,
index: number,
): Promise<void> {
const entry = target.entry as CifsBackupTarget
const action = await this.actionCtrl.create({
@@ -93,8 +115,12 @@ export class BackupDrivesComponent {
},
},
{
text: this.type === 'create' ? 'Create Backup' : 'Restore From Backup',
icon: this.type === 'create' ? 'cloud-upload-outline' : 'cloud-download-outline',
text:
this.type === 'create' ? 'Create Backup' : 'Restore From Backup',
icon:
this.type === 'create'
? 'cloud-upload-outline'
: 'cloud-download-outline',
handler: () => {
this.select(target)
},
@@ -105,7 +131,7 @@ export class BackupDrivesComponent {
await action.present()
}
private async presentAlertError (message: string): Promise<void> {
private async presentAlertError(message: string): Promise<void> {
const alert = await this.alertCtrl.create({
header: 'Error',
message,
@@ -114,7 +140,7 @@ export class BackupDrivesComponent {
await alert.present()
}
private async addCifs (value: RR.AddBackupTargetReq): Promise<boolean> {
private async addCifs(value: RR.AddBackupTargetReq): Promise<boolean> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Testing connectivity to shared folder...',
@@ -139,7 +165,11 @@ export class BackupDrivesComponent {
}
}
private async presentModalEditCifs (id: string, entry: CifsBackupTarget, index: number): Promise<void> {
private async presentModalEditCifs(
id: string,
entry: CifsBackupTarget,
index: number,
): Promise<void> {
const { hostname, path, username } = entry
const modal = await this.modalCtrl.create({
@@ -166,7 +196,10 @@ export class BackupDrivesComponent {
await modal.present()
}
private async editCifs (value: RR.UpdateBackupTargetReq, index: number): Promise<void> {
private async editCifs(
value: RR.UpdateBackupTargetReq,
index: number,
): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Testing connectivity to shared folder...',
@@ -185,7 +218,7 @@ export class BackupDrivesComponent {
}
}
private async deleteCifs (id: string, index: number): Promise<void> {
private async deleteCifs(id: string, index: number): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Removing...',
@@ -204,7 +237,6 @@ export class BackupDrivesComponent {
}
}
@Component({
selector: 'backup-drives-header',
templateUrl: './backup-drives-header.component.html',
@@ -214,16 +246,13 @@ export class BackupDrivesHeaderComponent {
@Input() title: string
@Output() onClose: EventEmitter<void> = new EventEmitter()
constructor (
public readonly backupService: BackupService,
) { }
constructor(public readonly backupService: BackupService) {}
refresh () {
refresh() {
this.backupService.getBackupTargets()
}
}
@Component({
selector: 'backup-drives-status',
templateUrl: './backup-drives-status.component.html',
@@ -238,7 +267,8 @@ const CifsSpec: ConfigSpec = {
hostname: {
type: 'string',
name: 'Hostname',
description: 'The hostname of your target device on the Local Area Network.',
description:
'The hostname of your target device on the Local Area Network.',
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
pattern: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
'pattern-description': `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
@@ -249,7 +279,8 @@ const CifsSpec: ConfigSpec = {
path: {
type: 'string',
name: 'Path',
description: 'The directory path to the shared folder on your target device.',
description:
'The directory path to the shared folder on your target device.',
placeholder: 'e.g. /Desktop/my-folder',
nullable: false,
masked: false,

View File

@@ -2,9 +2,13 @@ import { Injectable } from '@angular/core'
import { IonicSafeString } from '@ionic/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getErrorMessage } from 'src/app/services/error-toast.service'
import { BackupTarget, CifsBackupTarget, DiskBackupTarget } from 'src/app/services/api/api.types'
import { Emver } from 'src/app/services/emver.service'
import { MappedBackupTarget } from 'src/app/util/misc.util'
import {
BackupTarget,
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { Emver } from '@start9labs/shared'
@Injectable({
providedIn: 'root',
@@ -15,36 +19,36 @@ export class BackupService {
loading = true
loadingError: string | IonicSafeString
constructor (
constructor(
private readonly embassyApi: ApiService,
private readonly emver: Emver,
) { }
) {}
async getBackupTargets (): Promise<void> {
async getBackupTargets(): Promise<void> {
this.loading = true
try {
const targets = await this.embassyApi.getBackupTargets({ })
const targets = await this.embassyApi.getBackupTargets({})
// cifs
this.cifs = Object.entries(targets)
.filter(([_, target]) => target.type === 'cifs')
.map(([id, cifs]) => {
return {
id,
hasValidBackup: this.hasValidBackup(cifs),
entry: cifs as CifsBackupTarget,
}
})
.filter(([_, target]) => target.type === 'cifs')
.map(([id, cifs]) => {
return {
id,
hasValidBackup: this.hasValidBackup(cifs),
entry: cifs as CifsBackupTarget,
}
})
// drives
this.drives = Object.entries(targets)
.filter(([_, target]) => target.type === 'disk')
.map(([id, drive]) => {
return {
id,
hasValidBackup: this.hasValidBackup(drive),
entry: drive as DiskBackupTarget,
}
})
.filter(([_, target]) => target.type === 'disk')
.map(([id, drive]) => {
return {
id,
hasValidBackup: this.hasValidBackup(drive),
entry: drive as DiskBackupTarget,
}
})
} catch (e) {
this.loadingError = getErrorMessage(e)
} finally {
@@ -52,7 +56,9 @@ export class BackupService {
}
}
hasValidBackup (target: BackupTarget): boolean {
return [0, 1].includes(this.emver.compare(target['embassy-os']?.version, '0.3.0'))
hasValidBackup(target: BackupTarget): boolean {
return [0, 1].includes(
this.emver.compare(target['embassy-os']?.version, '0.3.0'),
)
}
}

View File

@@ -1,4 +1,11 @@
<div style="position: relative; margin-right: 1vh;">
<ion-badge mode="md" class="md-badge" *ngIf="unreadCount && !sidebarOpen" color="danger">{{ unreadCount }}</ion-badge>
<div class="wrapper">
<ion-badge
*ngIf="unreadCount && !sidebarOpen"
mode="md"
class="md-badge"
color="danger"
>
{{ unreadCount }}
</ion-badge>
<ion-menu-button color="dark"></ion-menu-button>
</div>

View File

@@ -1,18 +1,11 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { BadgeMenuComponent } from './badge-menu.component'
import { IonicModule } from '@ionic/angular'
import { SharingModule } from 'src/app/modules/sharing.module'
import { BadgeMenuComponent } from './badge-menu.component'
@NgModule({
declarations: [
BadgeMenuComponent,
],
imports: [
CommonModule,
IonicModule,
SharingModule,
],
declarations: [BadgeMenuComponent],
imports: [CommonModule, IonicModule],
exports: [BadgeMenuComponent],
})
export class BadgeMenuComponentModule { }
export class BadgeMenuComponentModule {}

View File

@@ -1,3 +1,8 @@
.wrapper {
position: relative;
margin-right: 1vh;
}
.md-badge {
background-color: var(--ion-color-danger);
position: absolute;

View File

@@ -1,29 +1,25 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormObjectComponent, FormLabelComponent, FormErrorComponent } from './form-object.component'
import {
FormObjectComponent,
FormLabelComponent,
FormErrorComponent,
} from './form-object.component'
import { IonicModule } from '@ionic/angular'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { SharingModule } from 'src/app/modules/sharing.module'
import { SharedPipesModule } from '@start9labs/shared'
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
@NgModule({
declarations: [
FormObjectComponent,
FormLabelComponent,
FormErrorComponent,
],
declarations: [FormObjectComponent, FormLabelComponent, FormErrorComponent],
imports: [
CommonModule,
IonicModule,
FormsModule,
ReactiveFormsModule,
SharingModule,
SharedPipesModule,
EnumListPageModule,
],
exports: [
FormObjectComponent,
FormLabelComponent,
FormErrorComponent,
],
exports: [FormObjectComponent, FormLabelComponent, FormErrorComponent],
})
export class FormObjectComponentModule { }
export class FormObjectComponentModule {}

View File

@@ -1,11 +1,28 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { AbstractFormGroupDirective, FormArray, FormGroup } from '@angular/forms'
import { AlertButton, AlertController, IonicSafeString, ModalController } from '@ionic/angular'
import { ConfigSpec, ListValueSpecOf, ValueSpec, ValueSpecBoolean, ValueSpecList, ValueSpecListOf, ValueSpecUnion } from 'src/app/pkg-config/config-types'
import {
AbstractFormGroupDirective,
FormArray,
FormGroup,
} from '@angular/forms'
import {
AlertButton,
AlertController,
IonicSafeString,
ModalController,
} from '@ionic/angular'
import {
ConfigSpec,
ListValueSpecOf,
ValueSpec,
ValueSpecBoolean,
ValueSpecList,
ValueSpecListOf,
ValueSpecUnion,
} from 'src/app/pkg-config/config-types'
import { FormService } from 'src/app/services/form.service'
import { Range } from 'src/app/pkg-config/config-utilities'
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
import { pauseFor } from 'src/app/util/misc.util'
import { pauseFor } from '@start9labs/shared'
import { v4 } from 'uuid'
const Mustache = require('mustache')
@@ -22,31 +39,38 @@ export class FormObjectComponent {
@Input() showEdited: boolean = false
@Output() onInputChange = new EventEmitter<void>()
@Output() onExpand = new EventEmitter<void>()
warningAck: { [key: string]: boolean } = { }
unmasked: { [key: string]: boolean } = { }
objectDisplay: { [key: string]: { expanded: boolean, height: string } } = { }
objectListDisplay: { [key: string]: { expanded: boolean, height: string, displayAs: string }[] } = { }
warningAck: { [key: string]: boolean } = {}
unmasked: { [key: string]: boolean } = {}
objectDisplay: { [key: string]: { expanded: boolean; height: string } } = {}
objectListDisplay: {
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
} = {}
private objectId = v4()
Object = Object
constructor (
constructor(
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
) { }
) {}
ngOnInit () {
ngOnInit() {
Object.keys(this.objectSpec).forEach(key => {
const spec = this.objectSpec[key]
if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) {
this.objectListDisplay[key] = [];
(this.formGroup.get(key).value as any[]).forEach((obj, index) => {
const displayAs = (spec.spec as ListValueSpecOf<'object'>)['display-as']
this.objectListDisplay[key] = []
this.formGroup.get(key).value.forEach((obj, index) => {
const displayAs = (spec.spec as ListValueSpecOf<'object'>)[
'display-as'
]
this.objectListDisplay[key][index] = {
expanded: false,
height: '0px',
displayAs: displayAs ? (Mustache as any).render(displayAs, obj) : '',
displayAs: displayAs
? (Mustache as any).render(displayAs, obj)
: '',
}
})
} else if (['object', 'union'].includes(spec.type)) {
@@ -58,11 +82,11 @@ export class FormObjectComponent {
})
}
getEnumListDisplay (arr: string[], spec: ListValueSpecOf<'enum'>): string {
getEnumListDisplay(arr: string[], spec: ListValueSpecOf<'enum'>): string {
return arr.map((v: string) => spec['value-names'][v]).join(', ')
}
updateUnion (e: any): void {
updateUnion(e: any): void {
const primary = this.unionSpec.tag.id
Object.keys(this.formGroup.controls).forEach(control => {
@@ -70,26 +94,31 @@ export class FormObjectComponent {
this.formGroup.removeControl(control)
})
const unionGroup = this.formService.getUnionObject(this.unionSpec as ValueSpecUnion, e.detail.value)
const unionGroup = this.formService.getUnionObject(
this.unionSpec as ValueSpecUnion,
e.detail.value,
)
Object.keys(unionGroup.controls).forEach(control => {
if (control === primary) return
this.formGroup.addControl(control, unionGroup.controls[control])
})
Object.entries(this.unionSpec.variants[e.detail.value]).forEach(([key, value]) => {
if (['object', 'union'].includes(value.type)) {
this.objectDisplay[key] = {
expanded: false,
height: '0px',
Object.entries(this.unionSpec.variants[e.detail.value]).forEach(
([key, value]) => {
if (['object', 'union'].includes(value.type)) {
this.objectDisplay[key] = {
expanded: false,
height: '0px',
}
}
}
})
},
)
this.onExpand.emit()
}
resize (key: string, i?: number): void {
resize(key: string, i?: number): void {
setTimeout(() => {
if (i !== undefined) {
this.objectListDisplay[key][i].height = this.getDocSize(key, i)
@@ -100,11 +129,11 @@ export class FormObjectComponent {
}, 250) // 250 to match transition-duration, defined in html
}
addListItemWrapper (key: string, spec: ValueSpec) {
addListItemWrapper(key: string, spec: ValueSpec) {
this.presentAlertChangeWarning(key, spec, () => this.addListItem(key))
}
addListItem (key: string, markDirty = true, val?: string): void {
addListItem(key: string, markDirty = true, val?: string): void {
const arr = this.formGroup.get(key) as FormArray
if (markDirty) arr.markAsDirty()
const listSpec = this.objectSpec[key] as ValueSpecList
@@ -112,7 +141,9 @@ export class FormObjectComponent {
newItem.markAllAsTouched()
arr.insert(0, newItem)
if (['object', 'union'].includes(listSpec.subtype)) {
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)['display-as']
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)[
'display-as'
]
this.objectListDisplay[key].unshift({
height: '0px',
expanded: true,
@@ -125,30 +156,39 @@ export class FormObjectComponent {
}
}
toggleExpandObject (key: string) {
toggleExpandObject(key: string) {
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
this.objectDisplay[key].height = this.objectDisplay[key].expanded ? this.getDocSize(key) : '0px'
this.objectDisplay[key].height = this.objectDisplay[key].expanded
? this.getDocSize(key)
: '0px'
this.onExpand.emit()
}
toggleExpandListObject (key: string, i: number) {
this.objectListDisplay[key][i].expanded = !this.objectListDisplay[key][i].expanded
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i].expanded ? this.getDocSize(key, i) : '0px'
toggleExpandListObject(key: string, i: number) {
this.objectListDisplay[key][i].expanded =
!this.objectListDisplay[key][i].expanded
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
.expanded
? this.getDocSize(key, i)
: '0px'
}
updateLabel (key: string, i: number, displayAs: string) {
this.objectListDisplay[key][i].displayAs = displayAs ? Mustache.render(displayAs, this.formGroup.get(key).value[i]) : ''
updateLabel(key: string, i: number, displayAs: string) {
this.objectListDisplay[key][i].displayAs = displayAs
? Mustache.render(displayAs, this.formGroup.get(key).value[i])
: ''
}
getWarningText (text: string): IonicSafeString {
if (text) return new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
getWarningText(text: string): IonicSafeString {
if (text)
return new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
}
handleInputChange () {
handleInputChange() {
this.onInputChange.emit()
}
handleBooleanChange (key: string, spec: ValueSpecBoolean) {
handleBooleanChange(key: string, spec: ValueSpecBoolean) {
if (spec.warning) {
const current = this.formGroup.get(key).value
const cancelFn = () => this.formGroup.get(key).setValue(!current)
@@ -156,7 +196,11 @@ export class FormObjectComponent {
}
}
async presentModalEnumList (key: string, spec: ValueSpecListOf<'enum'>, current: string[]) {
async presentModalEnumList(
key: string,
spec: ValueSpecListOf<'enum'>,
current: string[],
) {
const modal = await this.modalCtrl.create({
componentProps: {
key,
@@ -175,7 +219,12 @@ export class FormObjectComponent {
await modal.present()
}
async presentAlertChangeWarning (key: string, spec: ValueSpec, okFn?: Function, cancelFn?: Function) {
async presentAlertChangeWarning(
key: string,
spec: ValueSpec,
okFn?: Function,
cancelFn?: Function,
) {
if (!spec.warning || this.warningAck[key]) return okFn ? okFn() : null
this.warningAck[key] = true
@@ -207,7 +256,7 @@ export class FormObjectComponent {
await alert.present()
}
async presentAlertDelete (key: string, index: number) {
async presentAlertDelete(key: string, index: number) {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: 'Are you sure you want to delete this entry?',
@@ -228,17 +277,19 @@ export class FormObjectComponent {
await alert.present()
}
private deleteListItem (key: string, index: number, markDirty = true): void {
if (this.objectListDisplay[key]) this.objectListDisplay[key][index].height = '0px'
private deleteListItem(key: string, index: number, markDirty = true): void {
if (this.objectListDisplay[key])
this.objectListDisplay[key][index].height = '0px'
const arr = this.formGroup.get(key) as FormArray
if (markDirty) arr.markAsDirty()
pauseFor(250).then(() => {
if (this.objectListDisplay[key]) this.objectListDisplay[key].splice(index, 1)
if (this.objectListDisplay[key])
this.objectListDisplay[key].splice(index, 1)
arr.removeAt(index)
})
}
private updateEnumList (key: string, current: string[], updated: string[]) {
private updateEnumList(key: string, current: string[], updated: string[]) {
this.formGroup.get(key).markAsDirty()
for (let i = current.length - 1; i >= 0; i--) {
@@ -254,17 +305,16 @@ export class FormObjectComponent {
})
}
private getDocSize (key: string, index = 0) {
private getDocSize(key: string, index = 0) {
const element = document.getElementById(this.getElementId(key, index))
return `${element.scrollHeight}px`
}
getElementId (key: string, index = 0): string {
getElementId(key: string, index = 0): string {
return `${key}-${index}-${this.objectId}`
}
async presentUnionTagDescription (name: string, description: string) {
async presentUnionTagDescription(name: string, description: string) {
const alert = await this.alertCtrl.create({
header: name,
message: description,
@@ -272,7 +322,7 @@ export class FormObjectComponent {
await alert.present()
}
asIsOrder () {
asIsOrder() {
return 0
}
}
@@ -293,11 +343,9 @@ export class FormLabelComponent {
Range = Range
@Input() data: HeaderData
constructor (
private readonly alertCtrl: AlertController,
) { }
constructor(private readonly alertCtrl: AlertController) {}
async presentAlertDescription () {
async presentAlertDescription() {
const { name, description } = this.data.spec
const alert = await this.alertCtrl.create({
@@ -308,7 +356,6 @@ export class FormLabelComponent {
}
}
@Component({
selector: 'form-error',
templateUrl: './form-error.component.html',

View File

@@ -3,18 +3,16 @@ import { CommonModule } from '@angular/common'
import { AlertComponent } from './alert.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { MarkdownPipeModule } from '@start9labs/shared'
@NgModule({
declarations: [
AlertComponent,
],
declarations: [AlertComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
MarkdownPipeModule,
],
exports: [AlertComponent],
})
export class AlertComponentModule { }
export class AlertComponentModule {}

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { BehaviorSubject, from, Subject } from 'rxjs'
import { takeUntil } from 'rxjs/operators'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { capitalizeFirstLetter } from '@start9labs/shared'
import { markAsLoadingDuring$ } from '../loadable'
import { WizardAction } from '../wizard-types'
@@ -30,16 +30,21 @@ export class CompleteComponent {
message: string
load () {
markAsLoadingDuring$(this.loading$, from(this.params.executeAction())).pipe(takeUntil(this.cancel$)).subscribe(
{
error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
load() {
markAsLoadingDuring$(this.loading$, from(this.params.executeAction()))
.pipe(takeUntil(this.cancel$))
.subscribe({
error: e =>
this.transitions.error(
new Error(`${this.params.action} failed: ${e.message || e}`),
),
complete: () => this.transitions.final(),
},
)
})
}
ngOnInit () {
this.message = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
ngOnInit() {
this.message = `${capitalizeFirstLetter(this.params.verb)} ${
this.params.title
}...`
}
}

View File

@@ -3,18 +3,16 @@ import { CommonModule } from '@angular/common'
import { DependentsComponent } from './dependents.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { SharedPipesModule } from '@start9labs/shared'
@NgModule({
declarations: [
DependentsComponent,
],
declarations: [DependentsComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
SharedPipesModule,
],
exports: [DependentsComponent],
})
export class DependentsComponentModule { }
export class DependentsComponentModule {}

View File

@@ -3,7 +3,7 @@ import { BehaviorSubject, from, Subject } from 'rxjs'
import { takeUntil, tap } from 'rxjs/operators'
import { Breakages } from 'src/app/services/api/api.types'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { capitalizeFirstLetter, isEmptyObject } from 'src/app/util/misc.util'
import { capitalizeFirstLetter, isEmptyObject } from '@start9labs/shared'
import { markAsLoadingDuring$ } from '../loadable'
import { WizardAction } from '../wizard-types'
@@ -14,9 +14,9 @@ import { WizardAction } from '../wizard-types'
})
export class DependentsComponent {
@Input() params: {
title: string,
action: WizardAction, //Are you sure you want to *uninstall*...,
verb: string, // *Uninstalling* will cause problems...
title: string
action: WizardAction //Are you sure you want to *uninstall*...,
verb: string // *Uninstalling* will cause problems...
fetchBreakages: () => Promise<Breakages>
}
@Input() transitions: {
@@ -32,27 +32,37 @@ export class DependentsComponent {
loading$ = new BehaviorSubject(false)
cancel$ = new Subject<void>()
constructor (
public readonly patch: PatchDbService,
) { }
constructor(public readonly patch: PatchDbService) {}
load () {
load() {
markAsLoadingDuring$(this.loading$, from(this.params.fetchBreakages()))
.pipe(
takeUntil(this.cancel$),
tap(breakages => this.dependentBreakages = breakages),
)
.subscribe(
{
.pipe(
takeUntil(this.cancel$),
tap(breakages => (this.dependentBreakages = breakages)),
)
.subscribe({
complete: () => {
if (this.dependentBreakages && !isEmptyObject(this.dependentBreakages)) {
this.dependentViolation = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will prohibit the following services from functioning properly and may cause them to stop if they are currently running.`
if (
this.dependentBreakages &&
!isEmptyObject(this.dependentBreakages)
) {
this.dependentViolation = `${capitalizeFirstLetter(
this.params.verb,
)} ${
this.params.title
} will prohibit the following services from functioning properly and may cause them to stop if they are currently running.`
} else {
this.transitions.next()
}
},
error: (e: Error) => this.transitions.error(new Error(`Fetching dependent service information failed: ${e.message || e}`)),
},
)
error: (e: Error) =>
this.transitions.error(
new Error(
`Fetching dependent service information failed: ${
e.message || e
}`,
),
),
})
}
}

View File

@@ -3,21 +3,19 @@ import { CommonModule } from '@angular/common'
import { InstallWizardComponent } from './install-wizard.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { EmverPipesModule } from '@start9labs/shared'
import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module'
import { NotesComponentModule } from './notes/notes.component.module'
import { AlertComponentModule } from './alert/alert.component.module'
@NgModule({
declarations: [
InstallWizardComponent,
],
declarations: [InstallWizardComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
EmverPipesModule,
DependentsComponentModule,
CompleteComponentModule,
NotesComponentModule,
@@ -25,4 +23,4 @@ import { AlertComponentModule } from './alert/alert.component.module'
],
exports: [InstallWizardComponent],
})
export class InstallWizardComponentModule { }
export class InstallWizardComponentModule {}

View File

@@ -1,6 +1,13 @@
import { Component, Input, NgZone, QueryList, ViewChild, ViewChildren } from '@angular/core'
import {
Component,
Input,
NgZone,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core'
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
import { capitalizeFirstLetter, pauseFor } from '@start9labs/shared'
import { CompleteComponent } from './complete/complete.component'
import { DependentsComponent } from './dependents/dependents.component'
import { AlertComponent } from './alert/alert.component'
@@ -30,54 +37,70 @@ export class InstallWizardComponent {
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
@ViewChildren('components')
slideComponentsQL: QueryList<Loadable>
get slideComponents (): Loadable[] { return this.slideComponentsQL.toArray() }
get slideComponents(): Loadable[] {
return this.slideComponentsQL.toArray()
}
private slideIndex = 0
get currentSlide (): Loadable {
get currentSlide(): Loadable {
return this.slideComponents[this.slideIndex]
}
get currentBottomBar (): SlideDefinition['bottomBar'] {
get currentBottomBar(): SlideDefinition['bottomBar'] {
return this.params.slideDefinitions[this.slideIndex].bottomBar
}
initializing = true
error = ''
constructor (
constructor(
private readonly modalController: ModalController,
private readonly zone: NgZone,
) { }
) {}
ngAfterViewInit () {
ngAfterViewInit() {
this.currentSlide.load()
this.slideContainer.update()
this.slideContainer.lockSwipes(true)
}
ionViewDidEnter () {
ionViewDidEnter() {
this.initializing = false
}
// process bottom bar buttons
private transition = (info: { next: any } | { error: Error } | { cancelled: true } | { final: true }) => {
const i = info as { next?: any, error?: Error, cancelled?: true, final?: true }
private transition = (
info:
| { next: any }
| { error: Error }
| { cancelled: true }
| { final: true },
) => {
const i = info as {
next?: any
error?: Error
cancelled?: true
final?: true
}
if (i.cancelled) this.currentSlide.cancel$.next()
if (i.final || i.cancelled) return this.modalController.dismiss(i)
if (i.error) return this.error = capitalizeFirstLetter(i.error.message)
if (i.error) return (this.error = capitalizeFirstLetter(i.error.message))
this.moveToNextSlide(i.next)
}
// bottom bar button callbacks. Pass this into components if they need to trigger slide transitions independent of the bottom bar clicks
transitions = {
next: (prevResult: any) => this.transition({ next: prevResult || this.currentSlide.result }),
next: (prevResult: any) =>
this.transition({ next: prevResult || this.currentSlide.result }),
cancel: () => this.transition({ cancelled: true }),
final: () => this.transition({ final: true }),
error: (e: Error) => this.transition({ error: e }),
}
private async moveToNextSlide (prevResult?: any) {
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.transition({ final: true }) }
private async moveToNextSlide(prevResult?: any) {
if (this.slideComponents[this.slideIndex + 1] === undefined) {
return this.transition({ final: true })
}
this.zone.run(async () => {
this.slideComponents[this.slideIndex + 1].load(prevResult)
await pauseFor(50) // give the load ^ opportunity to propogate into slide before sliding it into view
@@ -89,7 +112,7 @@ export class InstallWizardComponent {
})
}
async callTransition (transition: Function) {
async callTransition(transition: Function) {
this.transitioning = true
await transition()
this.transitioning = false
@@ -98,14 +121,14 @@ export class InstallWizardComponent {
export interface SlideDefinition {
slide:
{ selector: 'dependents', params: DependentsComponent['params'] } |
{ selector: 'complete', params: CompleteComponent['params'] } |
{ selector: 'alert', params: AlertComponent['params'] } |
{ selector: 'notes', params: NotesComponent['params'] }
| { selector: 'dependents'; params: DependentsComponent['params'] }
| { selector: 'complete'; params: CompleteComponent['params'] }
| { selector: 'alert'; params: AlertComponent['params'] }
| { selector: 'notes'; params: NotesComponent['params'] }
bottomBar: {
cancel: {
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
afterLoading?: { text?: string },
afterLoading?: { text?: string }
whileLoading?: { text?: string }
}
// indicates the existence of next or finish buttons (should only have one)
@@ -114,11 +137,16 @@ export interface SlideDefinition {
}
}
export type TopbarParams = { action: WizardAction, title: string, version: string }
export type TopbarParams = {
action: WizardAction
title: string
version: string
}
export async function wizardModal (
modalController: ModalController, params: InstallWizardComponent['params'],
): Promise<{ cancelled?: true, final?: true, modal: HTMLIonModalElement }> {
export async function wizardModal(
modalController: ModalController,
params: InstallWizardComponent['params'],
): Promise<{ cancelled?: true; final?: true; modal: HTMLIonModalElement }> {
const modal = await modalController.create({
backdropDismiss: false,
cssClass: 'wizard-modal',

View File

@@ -3,18 +3,16 @@ import { CommonModule } from '@angular/common'
import { NotesComponent } from './notes.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { MarkdownPipeModule } from '@start9labs/shared'
@NgModule({
declarations: [
NotesComponent,
],
declarations: [NotesComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
MarkdownPipeModule,
],
exports: [NotesComponent],
})
export class NotesComponentModule { }
export class NotesComponentModule {}

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Breakages } from 'src/app/services/api/api.types'
import { exists } from 'src/app/util/misc.util'
import { exists } from '@start9labs/shared'
import { ApiService } from '../../services/api/embassy-api.service'
import {
InstallWizardComponent,

View File

@@ -2,15 +2,11 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { LogsPage } from './logs.page'
import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from '@start9labs/shared'
@NgModule({
declarations: [LogsPage],
imports: [
CommonModule,
IonicModule,
SharingModule,
],
imports: [CommonModule, IonicModule, TextSpinnerComponentModule],
exports: [LogsPage],
})
export class LogsPageModule { }
export class LogsPageModule {}

View File

@@ -1,13 +1,21 @@
<p
[style.color]="disconnected ? 'gray' : 'var(--ion-color-' + rendering.color + ')'"
[style.color]="
disconnected ? 'gray' : 'var(--ion-color-' + rendering.color + ')'
"
[style.font-size]="size"
[style.font-style]="style"
[style.font-weight]="weight"
>
<span *ngIf= "!installProgress">
<span *ngIf="!installProgress">
{{ disconnected ? 'Unknown' : rendering.display }}
<span *ngIf="rendering.showDots" class="loading-dots"></span>
<span *ngIf="rendering.display === PR[PS.Stopping].display && (sigtermTimeout | durationToSeconds) > 30">This may take a while.</span>
<span
*ngIf="
rendering.display === PR[PS.Stopping].display &&
(sigtermTimeout | durationToSeconds) > 30
"
>This may take a while.</span
>
</span>
<span *ngIf="installProgress">
<span *ngIf="installProgress < 99">
@@ -19,5 +27,4 @@
<span class="loading-dots"></span>
</span>
</span>
</p>

View File

@@ -1,18 +1,12 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { StatusComponent } from './status.component'
import { IonicModule } from '@ionic/angular'
import { SharingModule } from 'src/app/modules/sharing.module'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { StatusComponent } from './status.component'
@NgModule({
declarations: [
StatusComponent,
],
imports: [
CommonModule,
IonicModule,
SharingModule,
],
declarations: [StatusComponent],
imports: [CommonModule, IonicModule, UnitConversionPipesModule],
exports: [StatusComponent],
})
export class StatusComponentModule { }
export class StatusComponentModule {}

View File

@@ -1,5 +1,9 @@
import { Component, Input } from '@angular/core'
import { PrimaryRendering, PrimaryStatus, StatusRendering } from 'src/app/services/pkg-status-rendering.service'
import {
PrimaryRendering,
PrimaryStatus,
StatusRendering,
} from 'src/app/services/pkg-status-rendering.service'
@Component({
selector: 'status',
@@ -18,4 +22,3 @@ export class StatusComponent {
@Input() installProgress?: number
@Input() sigtermTimeout?: string
}

View File

@@ -1,8 +0,0 @@
<ion-grid style="height: 100%;">
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
<ion-col>
<ion-spinner name="lines" color="warning"></ion-spinner>
<p>{{ text }}</p>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { TextSpinnerComponent } from './text-spinner.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
@NgModule({
declarations: [
TextSpinnerComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
],
exports: [TextSpinnerComponent],
})
export class TextSpinnerComponentModule { }

View File

@@ -1,10 +0,0 @@
import { Component, Input } from '@angular/core'
@Component({
selector: 'text-spinner',
templateUrl: './text-spinner.component.html',
styleUrls: ['./text-spinner.component.scss'],
})
export class TextSpinnerComponent {
@Input() text: string
}