revamp wifi, fix error messaging in forms

This commit is contained in:
Matt Hill
2021-08-17 19:01:35 -06:00
parent 0f43414e25
commit 70a9bc96e8
46 changed files with 899 additions and 781 deletions

70
ui/package-lock.json generated
View File

@@ -16,7 +16,7 @@
"@angular/router": "12.2.0", "@angular/router": "12.2.0",
"@ionic/angular": "5.6.13", "@ionic/angular": "5.6.13",
"@ionic/storage-angular": "3.0.6", "@ionic/storage-angular": "3.0.6",
"@start9labs/emver": "0.1.4", "@start9labs/emver": "0.1.5",
"ajv": "6.12.6", "ajv": "6.12.6",
"compare-versions": "3.6.0", "compare-versions": "3.6.0",
"core-js": "3.16.1", "core-js": "3.16.1",
@@ -54,16 +54,16 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"rxjs": "^6.6.3", "rxjs": "6.6.7",
"sorted-btree": "^1.5.0", "sorted-btree": "1.5.0",
"uuid": "^8.3.2" "uuid": "8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^15.0.0", "@types/node": "16.4.13",
"@types/uuid": "^8.3.0", "@types/uuid": "8.3.1",
"ts-node": "^9.1.1", "ts-node": "10.2.0",
"tslint": "^6.1.0", "tslint": "6.1.3",
"typescript": "4.1.5" "typescript": "4.3.5"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -2861,9 +2861,9 @@
"dev": true "dev": true
}, },
"node_modules/@start9labs/emver": { "node_modules/@start9labs/emver": {
"version": "0.1.4", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz",
"integrity": "sha512-lWhc94tGhWjJYHTYlHydHESViBi8DqjIqdwtPKMeVkGp3RKZdcv8RRpvEatQHlnCoT5IfKuH/29BZtLnwGL4CQ==" "integrity": "sha512-1dhiG03VkfEwSLx/JPKVms6srAbYFQgwfSGhwpUKMDliMXuAHGVaueStmqzVxn3JpH/HEVz0QW8w/PXHqjdiIg=="
}, },
"node_modules/@stencil/core": { "node_modules/@stencil/core": {
"version": "2.6.0", "version": "2.6.0",
@@ -16820,9 +16820,7 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz",
"integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==",
"dev": true, "dev": true,
"requires": { "requires": {}
"ajv": "^8.0.0"
}
}, },
"core-js": { "core-js": {
"version": "3.16.0", "version": "3.16.0",
@@ -16900,9 +16898,7 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz",
"integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==",
"dev": true, "dev": true,
"requires": { "requires": {}
"ajv": "^8.0.0"
}
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
@@ -16946,9 +16942,7 @@
"integrity": "sha512-Brah4Uo5/U8v76c6euTwtjVFFaVishwnJrQBYpev1JRh4vjA1F4HY3UzQez41YUCszUCXKagG8v6eVRBHV1gkw==", "integrity": "sha512-Brah4Uo5/U8v76c6euTwtjVFFaVishwnJrQBYpev1JRh4vjA1F4HY3UzQez41YUCszUCXKagG8v6eVRBHV1gkw==",
"dev": true, "dev": true,
"peer": true, "peer": true,
"requires": { "requires": {}
"ajv": "^8.0.0"
}
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
@@ -17001,9 +16995,7 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz",
"integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==",
"dev": true, "dev": true,
"requires": { "requires": {}
"ajv": "^8.0.0"
}
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
@@ -17081,9 +17073,7 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz",
"integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==",
"dev": true, "dev": true,
"requires": { "requires": {}
"ajv": "^8.0.0"
}
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
@@ -18632,9 +18622,7 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz",
"integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==",
"dev": true, "dev": true,
"requires": { "requires": {}
"ajv": "^8.0.0"
}
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
@@ -18645,9 +18633,9 @@
} }
}, },
"@start9labs/emver": { "@start9labs/emver": {
"version": "0.1.4", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz",
"integrity": "sha512-lWhc94tGhWjJYHTYlHydHESViBi8DqjIqdwtPKMeVkGp3RKZdcv8RRpvEatQHlnCoT5IfKuH/29BZtLnwGL4CQ==" "integrity": "sha512-1dhiG03VkfEwSLx/JPKVms6srAbYFQgwfSGhwpUKMDliMXuAHGVaueStmqzVxn3JpH/HEVz0QW8w/PXHqjdiIg=="
}, },
"@stencil/core": { "@stencil/core": {
"version": "2.6.0", "version": "2.6.0",
@@ -24503,14 +24491,14 @@
"patch-db-client": { "patch-db-client": {
"version": "file:../../patch-db/client", "version": "file:../../patch-db/client",
"requires": { "requires": {
"@types/node": "^15.0.0", "@types/node": "16.4.13",
"@types/uuid": "^8.3.0", "@types/uuid": "8.3.1",
"rxjs": "^6.6.3", "rxjs": "6.6.7",
"sorted-btree": "^1.5.0", "sorted-btree": "1.5.0",
"ts-node": "^9.1.1", "ts-node": "10.2.0",
"tslint": "^6.1.0", "tslint": "6.1.3",
"typescript": "4.1.5", "typescript": "4.3.5",
"uuid": "^8.3.2" "uuid": "8.3.2"
} }
}, },
"path-dirname": { "path-dirname": {

View File

@@ -20,7 +20,7 @@
"@angular/router": "12.2.0", "@angular/router": "12.2.0",
"@ionic/angular": "5.6.13", "@ionic/angular": "5.6.13",
"@ionic/storage-angular": "3.0.6", "@ionic/storage-angular": "3.0.6",
"@start9labs/emver": "0.1.4", "@start9labs/emver": "0.1.5",
"ajv": "6.12.6", "ajv": "6.12.6",
"compare-versions": "3.6.0", "compare-versions": "3.6.0",
"core-js": "3.16.1", "core-js": "3.16.1",

View File

@@ -77,6 +77,7 @@
<ion-icon name="cube-outline"></ion-icon> <ion-icon name="cube-outline"></ion-icon>
<ion-icon name="desktop-outline"></ion-icon> <ion-icon name="desktop-outline"></ion-icon>
<ion-icon name="download-outline"></ion-icon> <ion-icon name="download-outline"></ion-icon>
<ion-icon name="earth-outline"></ion-icon>
<ion-icon name="ellipse"></ion-icon> <ion-icon name="ellipse"></ion-icon>
<ion-icon name="eye-off-outline"></ion-icon> <ion-icon name="eye-off-outline"></ion-icon>
<ion-icon name="eye-outline"></ion-icon> <ion-icon name="eye-outline"></ion-icon>
@@ -87,6 +88,7 @@
<ion-icon name="help-circle-outline"></ion-icon> <ion-icon name="help-circle-outline"></ion-icon>
<ion-icon name="home-outline"></ion-icon> <ion-icon name="home-outline"></ion-icon>
<ion-icon name="information-circle-outline"></ion-icon> <ion-icon name="information-circle-outline"></ion-icon>
<ion-icon name="key-outline"></ion-icon>
<ion-icon name="list-outline"></ion-icon> <ion-icon name="list-outline"></ion-icon>
<ion-icon name="logo-bitcoin"></ion-icon> <ion-icon name="logo-bitcoin"></ion-icon>
<ion-icon name="mail-outline"></ion-icon> <ion-icon name="mail-outline"></ion-icon>

View File

@@ -0,0 +1,31 @@
<div [hidden]="!control.dirty && !control.touched" class="validation-error">
<!-- primitive -->
<p *ngIf="control.hasError('required')">
{{ spec.name }} is required
</p>
<!-- string -->
<p *ngIf="control.hasError('pattern')">
{{ spec['pattern-description'] }}
</p>
<!-- number -->
<ng-container *ngIf="spec.type === 'number'">
<p *ngIf="control.hasError('numberNotInteger')">
{{ spec.name }} must be an integer
</p>
<p *ngIf="control.hasError('numberNotInRange')">
Number not in range
</p>
</ng-container>
<!-- list -->
<ng-container *ngIf="spec.type === 'list'">
<p *ngIf="control.hasError('listNotInRange')">
List not in range
</p>
<p *ngIf="control.hasError('listNotUnique')">
{{ spec['pattern-description'] }}
</p>
</ng-container>
</div>

View File

@@ -151,11 +151,12 @@
<ion-icon slot="icon-only" name="close"></ion-icon> <ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button> </ion-button>
</ion-item> </ion-item>
<ng-container *ngFor="let validation of formService.validationMessages[entry.key + '/' + i]"> <form-error
<p style="font-size: small;" *ngIf="abstractControl.hasError(validation.type) && (abstractControl.dirty || abstractControl.touched)"> *ngIf="abstractControl.errors"
<ion-text color="danger">{{ validation.message }}</ion-text> [control]="abstractControl"
</p> [spec]="spec.spec"
</ng-container> >
</form-error>
</ion-item-group> </ion-item-group>
</div> </div>
</div> </div>
@@ -183,11 +184,12 @@
</ion-item> </ion-item>
</ng-container> </ng-container>
</ng-container> </ng-container>
<div *ngFor="let validation of formService.validationMessages[entry.key]"> <form-error
<p class="validation-error" *ngIf="formGroup.get(entry.key).hasError(validation.type)"> *ngIf="formGroup.get(entry.key).errors"
<ion-text *ngIf="(formGroup.get(entry.key).dirty || formGroup.get(entry.key).touched)" color="danger">{{ spec.name }}: {{ validation.message }}</ion-text> [control]="formGroup.get(entry.key)"
</p> [spec]="spec"
</div> >
</form-error>
</ng-container> </ng-container>
</div> </div>
</ion-item-group> </ion-item-group>

View File

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

View File

@@ -27,5 +27,6 @@ ion-item-divider {
.validation-error { .validation-error {
p { p {
font-size: small; font-size: small;
color: var(--ion-color-danger);
} }
} }

View File

@@ -1,5 +1,5 @@
import { Component, Input, SimpleChange } from '@angular/core' import { Component, Input, SimpleChange } from '@angular/core'
import { FormArray, FormGroup } from '@angular/forms' import { AbstractFormGroupDirective, FormArray, FormGroup } from '@angular/forms'
import { AlertController, ModalController } from '@ionic/angular' import { AlertController, ModalController } from '@ionic/angular'
import { ConfigSpec, ListValueSpecOf, ValueSpec, ValueSpecList, ValueSpecListOf, ValueSpecUnion } from 'src/app/pkg-config/config-types' import { ConfigSpec, ListValueSpecOf, ValueSpec, ValueSpecList, ValueSpecListOf, ValueSpecUnion } from 'src/app/pkg-config/config-types'
import { FormService } from 'src/app/services/form.service' import { FormService } from 'src/app/services/form.service'
@@ -85,7 +85,7 @@ export class FormObjectComponent {
// const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length) // const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length)
// arr.push(new FormControl(value, validators)) // arr.push(new FormControl(value, validators))
const listSpec = this.objectSpec[key] as ValueSpecList const listSpec = this.objectSpec[key] as ValueSpecList
const newItem = this.formService.getListItem(key, arr.length, listSpec, val) const newItem = this.formService.getListItem(listSpec, val)
newItem.markAllAsTouched() newItem.markAllAsTouched()
arr.insert(0, newItem) arr.insert(0, newItem)
if (['object', 'union'].includes(listSpec.subtype)) { if (['object', 'union'].includes(listSpec.subtype)) {
@@ -222,3 +222,14 @@ export class FormLabelComponent {
await alert.present() await alert.present()
} }
} }
@Component({
selector: 'form-error',
templateUrl: './form-error.component.html',
styleUrls: ['./form-object.component.scss'],
})
export class FormErrorComponent {
@Input() control: AbstractFormGroupDirective
@Input() spec: ValueSpec
}

View File

@@ -1,41 +0,0 @@
import { Component, Input } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { ModalController } from '@ionic/angular'
import { Action } from 'src/app/services/patch-db/data-model'
import { FormService } from 'src/app/services/form.service'
@Component({
selector: 'app-action-input',
templateUrl: './app-action-input.page.html',
styleUrls: ['./app-action-input.page.scss'],
})
export class AppActionInputPage {
@Input() action: Action
actionForm: FormGroup
constructor (
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
) { }
ngOnInit () {
this.actionForm = this.formService.createForm(this.action['input-spec'])
}
async dismiss (): Promise<void> {
this.modalCtrl.dismiss()
}
async save (): Promise<void> {
if (this.actionForm.invalid) {
this.actionForm.markAllAsTouched()
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
return
}
this.modalCtrl.dismiss(this.actionForm.value)
}
asIsOrder () {
return 0
}
}

View File

@@ -1,12 +1,12 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppActionInputPage } from './app-action-input.page' import { GenericFormPage } from './generic-form.page'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module' import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module'
@NgModule({ @NgModule({
declarations: [AppActionInputPage], declarations: [GenericFormPage],
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
@@ -14,7 +14,7 @@ import { FormObjectComponentModule } from 'src/app/components/form-object/form-o
ReactiveFormsModule, ReactiveFormsModule,
FormObjectComponentModule, FormObjectComponentModule,
], ],
entryComponents: [AppActionInputPage], entryComponents: [GenericFormPage],
exports: [AppActionInputPage], exports: [GenericFormPage],
}) })
export class AppActionInputPageModule { } export class GenericFormPageModule { }

View File

@@ -1,14 +1,14 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title>{{ action.name }}</ion-title> <ion-title>{{ title }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<form [formGroup]="actionForm" (ngSubmit)="save()" novalidate> <form [formGroup]="formGroup" (ngSubmit)="save()" novalidate>
<form-object <form-object
[objectSpec]="action['input-spec']" [objectSpec]="spec"
[formGroup]="actionForm" [formGroup]="formGroup"
></form-object> ></form-object>
</form> </form>
</ion-content> </ion-content>
@@ -16,13 +16,13 @@
<ion-footer> <ion-footer>
<ion-toolbar> <ion-toolbar>
<ion-buttons slot="start" class="ion-padding-start"> <ion-buttons slot="start" class="ion-padding-start">
<ion-button fill="outline" (click)="dismiss()"> <ion-button fill="clear" (click)="dismiss()">
Cancel Cancel
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
<ion-buttons slot="end" class="ion-padding-end"> <ion-buttons slot="end" class="ion-padding-end">
<ion-button fill="outline" color="primary" (click)="save()"> <ion-button *ngFor="let button of buttons" fill="clear" (click)="handleClick(button)">
Execute {{ button.text }}
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>

View File

@@ -0,0 +1,46 @@
import { Component, Input } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { ModalController } from '@ionic/angular'
import { FormService } from 'src/app/services/form.service'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
export interface ActionButton {
text: string
handler: (value: any) => Promise<boolean>
}
@Component({
selector: 'generic-form',
templateUrl: './generic-form.page.html',
styleUrls: ['./generic-form.page.scss'],
})
export class GenericFormPage {
@Input() title: string
@Input() spec: ConfigSpec
@Input() buttons: ActionButton[]
formGroup: FormGroup
constructor (
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
) { }
ngOnInit () {
this.formGroup = this.formService.createForm(this.spec)
}
async dismiss (): Promise<void> {
this.modalCtrl.dismiss()
}
async handleClick (button: ActionButton): Promise<void> {
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched()
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
return
}
const success = await button.handler(this.formGroup.value)
if (success !== false) this.modalCtrl.dismiss()
}
}

View File

@@ -5,7 +5,7 @@ import { IonicModule } from '@ionic/angular'
import { AppActionsPage, AppActionsItemComponent } from './app-actions.page' import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
import { AppActionInputPageModule } from 'src/app/modals/app-action-input/app-action-input.module' import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
import { AppRestoreComponentModule } from 'src/app/modals/app-restore/app-restore.component.module' import { AppRestoreComponentModule } from 'src/app/modals/app-restore/app-restore.component.module'
const routes: Routes = [ const routes: Routes = [
@@ -22,7 +22,7 @@ const routes: Routes = [
RouterModule.forChild(routes), RouterModule.forChild(routes),
QRComponentModule, QRComponentModule,
SharingModule, SharingModule,
AppActionInputPageModule, GenericFormPageModule,
AppRestoreComponentModule, AppRestoreComponentModule,
], ],
declarations: [ declarations: [

View File

@@ -7,7 +7,7 @@ import { Action, Manifest, PackageDataEntry, PackageMainStatus } from 'src/app/s
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { AppActionInputPage } from 'src/app/modals/app-action-input/app-action-input.page' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component' import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component'
@@ -50,15 +50,20 @@ export class AppActionsPage {
if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) { if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) {
if (action.value['input-spec']) { if (action.value['input-spec']) {
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
component: AppActionInputPage, component: GenericFormPage,
componentProps: { componentProps: {
action: action.value, title: action.value.name,
spec: action.value['input-spec'],
buttons: [
{
text: 'Execute',
handler: (value: any) => {
return this.executeAction(pkg.manifest.id, action.key, value)
},
},
],
}, },
}) })
modal.onWillDismiss().then(({ data }) => {
if (!data) return
this.executeAction(pkg.manifest.id, action.key, data)
})
await modal.present() await modal.present()
} else { } else {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
@@ -135,7 +140,7 @@ export class AppActionsPage {
return this.navCtrl.navigateRoot('/services') return this.navCtrl.navigateRoot('/services')
} }
private async executeAction (pkgId: string, actionId: string, input?: object): Promise<void> { private async executeAction (pkgId: string, actionId: string, input?: object): Promise<boolean> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Executing action...', message: 'Executing action...',
@@ -155,9 +160,12 @@ export class AppActionsPage {
message: res.message.split('\n').join('</br ></br />'), message: res.message.split('\n').join('</br ></br />'),
buttons: ['OK'], buttons: ['OK'],
}) })
await successAlert.present()
setTimeout(() => successAlert.present(), 400)
} catch (e) { } catch (e) {
this.errToast.present(e) this.errToast.present(e)
return false
} finally { } finally {
loader.dismiss() loader.dismiss()
} }

View File

@@ -3,9 +3,14 @@
<ion-grid style="height: 100%;"> <ion-grid style="height: 100%;">
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;"> <ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
<ion-col> <ion-col>
<ion-spinner name="lines" color="warning"></ion-spinner> <ng-container *ngIf="status === ServerStatus.Updating">
<p *ngIf="status === ServerStatus.Updating">Embassy is updating</p> <h1>Embassy Updating</h1>
<p *ngIf="status === ServerStatus.BackingUp">Embassy is backing up</p> <img src="assets/img/gifs/cube.gif" />
</ng-container>
<ng-container *ngIf="status === ServerStatus.BackingUp">
<h1>Backing Up</h1>
<img src="assets/img/gifs/backing-up.gif" />
</ng-container>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>

View File

@@ -0,0 +1,4 @@
img {
width: 20%;
border-radius: 0;
}

View File

@@ -22,9 +22,9 @@ export class SessionsPage {
async ngOnInit () { async ngOnInit () {
try { try {
this.sessionInfo = await this.embassyApi.getSessions({}) this.sessionInfo = await this.embassyApi.getSessions({ })
} catch (e) { } catch (e) {
this.errToast.present(e.message) this.errToast.present(e)
} finally { } finally {
this.loading = false this.loading = false
} }

View File

@@ -8,9 +8,9 @@
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<!-- always -->
<ion-item-group> <ion-item-group>
<!-- always -->
<ion-item> <ion-item>
<ion-label> <ion-label>
<h2> <h2>
@@ -22,7 +22,7 @@
<ion-item-divider>Saved Keys</ion-item-divider> <ion-item-divider>Saved Keys</ion-item-divider>
<ion-item button detail="false" (click)="serverConfig.presentInputModal('ssh')"> <ion-item button detail="false" (click)="serverConfig.presentModalInput('ssh')">
<ion-icon slot="start" name="add" size="large"></ion-icon> <ion-icon slot="start" name="add" size="large"></ion-icon>
<ion-label>Add new key</ion-label> <ion-label>Add new key</ion-label>
</ion-item> </ion-item>

View File

@@ -24,7 +24,7 @@ const routes: Routes = [
}, },
{ {
path: 'wifi', path: 'wifi',
loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiListPageModule), loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiPageModule),
}, },
{ {
path: 'lan', path: 'lan',

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { WifiAddPage } from './wifi-add.page'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: WifiAddPage,
},
]
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
SharingModule,
],
declarations: [WifiAddPage],
})
export class WifiAddPageModule { }

View File

@@ -1,44 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Add Network</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group>
<ion-item>
<ion-label>Select Country</ion-label>
<ion-select slot="end" placeholder="Select" [(ngModel)]="countryCode" [selectedText]="countryCode">
<ion-select-option *ngFor="let country of countries | keyvalue : asIsOrder" [value]="country.key">
{{ country.key }} - {{ country.value }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item-divider>Network and Password</ion-item-divider>
<ion-item>
<ion-input placeholder="Network Name (SSID)" [(ngModel)]="ssid"></ion-input>
</ion-item>
<ion-item>
<ion-input type="password" placeholder="Password" [(ngModel)]="password"></ion-input>
</ion-item>
</ion-item-group>
<ion-grid style="margin-top: 40px;">
<ion-row>
<ion-col size="6">
<ion-button class="ion-text-wrap" [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="save()">
<p>Save</p>
</ion-button>
</ion-col>
<ion-col size="6">
<ion-button class="ion-text-wrap" [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="saveAndConnect()">
<p>Save & Connect</p>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -1,80 +0,0 @@
import { Component } from '@angular/core'
import { LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { WifiService } from '../wifi.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'wifi-add',
templateUrl: 'wifi-add.page.html',
styleUrls: ['wifi-add.page.scss'],
})
export class WifiAddPage {
countries = require('../../../../util/countries.json')
countryCode = 'US'
ssid = ''
password = ''
constructor (
private readonly navCtrl: NavController,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly wifiService: WifiService,
) { }
async save (): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.addWifi({
ssid: this.ssid,
password: this.password,
country: this.countryCode,
priority: 0,
connect: false,
})
this.navCtrl.back()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async saveAndConnect (): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Connecting. This could take while...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.addWifi({
ssid: this.ssid,
password: this.password,
country: this.countryCode,
priority: 0,
connect: true,
})
const success = this.wifiService.confirmWifi(this.ssid)
if (success) {
this.navCtrl.back()
}
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
asIsOrder (a: any, b: any) {
return 0
}
}

View File

@@ -2,17 +2,13 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { WifiListPage } from './wifi.page' import { WifiPage } from './wifi.page'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: WifiListPage, component: WifiPage,
},
{
path: 'add',
loadChildren: () => import('./wifi-add/wifi-add.module').then(m => m.WifiAddPageModule),
}, },
] ]
@@ -23,6 +19,6 @@ const routes: Routes = [
RouterModule.forChild(routes), RouterModule.forChild(routes),
SharingModule, SharingModule,
], ],
declarations: [WifiListPage], declarations: [WifiPage],
}) })
export class WifiListPageModule { } export class WifiPageModule { }

View File

@@ -4,30 +4,70 @@
<pwa-back-button></pwa-back-button> <pwa-back-button></pwa-back-button>
</ion-buttons> </ion-buttons>
<ion-title>WiFi Settings</ion-title> <ion-title>WiFi Settings</ion-title>
<ion-buttons slot="end">
<ion-button [routerLink]="['add']">
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<ion-item-group> <ion-item-group>
<!-- always -->
<ion-item> <ion-item>
<ion-label> <ion-label>
<p style="padding-bottom: 6px;">About</p> <h2>
<h2>Embassy will automatically connect to saved WiFi networks when they are available, allowing you to remove the Ethernet cable.</h2> Embassy will automatically connect to saved WiFi networks when they are available, allowing you to remove the Ethernet cable.
</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item-divider>Saved Networks</ion-item-divider> <ion-item-divider>Country</ion-item-divider>
<ng-container *ngIf="patch.data['server-info']?.wifi as wifi">
<ion-item button detail="false" *ngFor="let ssid of wifi.ssids" (click)="presentAction(ssid, wifi)"> <!-- loading -->
<ng-container *ngIf="loading">
<ion-item class="skeleton-parts">
<ion-button slot="start" fill="clear">
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
</ion-button>
<ion-label>
<ion-skeleton-text animated style="width: 150px;"></ion-skeleton-text>
</ion-label>
</ion-item>
</ng-container>
<!-- not loading -->
<ng-container *ngIf="!loading">
<ion-item button detail="false" (click)="presentAlertCountry()">
<ion-icon slot="start" name="earth-outline" size="large"></ion-icon>
<ion-label *ngIf="wifi.country">{{ wifi.country }} - {{ this.countries[wifi.country] }}</ion-label>
<ion-label *ngIf="!wifi.country">Select Country</ion-label>
</ion-item>
</ng-container>
<!-- loading -->
<ng-container *ngIf="loading">
<ion-item-divider>Saved Networks</ion-item-divider>
<ion-item *ngFor="let entry of ['', '']" class="skeleton-parts">
<ion-button slot="start" fill="clear">
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
</ion-button>
<ion-label>
<ion-skeleton-text animated style="width: 18%;"></ion-skeleton-text>
</ion-label>
</ion-item>
</ng-container>
<!-- not loading -->
<ng-container *ngIf="!loading && wifi.country">
<ion-item-divider>Saved Networks</ion-item-divider>
<ion-item button detail="false" (click)="presentModalAdd()">
<ion-icon slot="start" name="add" size="large"></ion-icon>
<ion-label>Add new network</ion-label>
</ion-item>
<ion-item button detail="false" *ngFor="let ssid of wifi.ssids" (click)="presentAction(ssid)">
<div *ngIf="ssid !== wifi.connected" slot="start" style="padding-right: 32px;"></div>
<ion-icon *ngIf="ssid === wifi.connected" slot="start" size="large" name="checkmark" color="success"></ion-icon>
<ion-label>{{ ssid }}</ion-label> <ion-label>{{ ssid }}</ion-label>
<ion-note slot="end" *ngIf="ssid === wifi.connected"><ion-text color="success">Connected</ion-text></ion-note> <img *ngIf="ssid === wifi.connected" slot="end" [src]="getWifiIcon()" style="max-width: 32px;" />
</ion-item> </ion-item>
</ng-container> </ng-container>
</ion-item-group> </ion-item-group>
</ion-content> </ion-content>

View File

@@ -0,0 +1,6 @@
.skeleton-parts {
ion-button::part(native) {
padding-inline-start: 0;
padding-inline-end: 0;
}
}

View File

@@ -1,44 +1,124 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { ActionSheetController, LoadingController } from '@ionic/angular' import { ActionSheetController, AlertController, LoadingController, ModalController, ToastController } from '@ionic/angular'
import { AlertInput } from '@ionic/core'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActionSheetButton } from '@ionic/core' import { ActionSheetButton } from '@ionic/core'
import { WifiService } from './wifi.service'
import { WiFiInfo } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Subscription } from 'rxjs'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { RR } from 'src/app/services/api/api.types'
import { pauseFor } from 'src/app/util/misc.util'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
@Component({ @Component({
selector: 'wifi', selector: 'wifi',
templateUrl: 'wifi.page.html', templateUrl: 'wifi.page.html',
styleUrls: ['wifi.page.scss'], styleUrls: ['wifi.page.scss'],
}) })
export class WifiListPage { export class WifiPage {
subs: Subscription[] = [] loading = true
wifi: RR.GetWifiRes = { } as any
countries = require('../../../util/countries.json') as { [key: string]: string }
constructor ( constructor (
private readonly embassyApi: ApiService, private readonly api: ApiService,
private readonly toastCtrl: ToastController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly actionCtrl: ActionSheetController, private readonly actionCtrl: ActionSheetController,
private readonly wifiService: WifiService,
public readonly patch: PatchDbService,
) { } ) { }
async presentAction (ssid: string, wifi: WiFiInfo) { async ngOnInit () {
try {
await this.getWifi()
} catch (e) {
this.errToast.present(e.message)
} finally {
this.loading = false
}
}
async getWifi (timeout?: number): Promise<void> {
this.wifi = await this.api.getWifi({ }, timeout)
if (!this.wifi.country) {
await this.presentAlertCountry()
}
}
async presentAlertCountry (): Promise<void> {
const inputs: AlertInput[] = Object.entries(this.countries).map(([country, fullName]) => {
return {
name: fullName,
type: 'radio',
label: `${country} - ${fullName}`,
value: country,
checked: country === this.wifi.country,
}
})
const alert = await this.alertCtrl.create({
header: 'Select Country',
inputs,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Save',
handler: async (country: string) => {
this.setCountry(country)
},
},
],
cssClass: 'wide-alert',
})
await alert.present()
}
async presentModalAdd () {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: wifiSpec.name,
spec: wifiSpec.spec,
buttons: [
{
text: 'Save',
handler: async (value: { ssid: string, password: string }) => {
await this.save(value.ssid, value.password)
},
},
{
text: 'Save and Connect',
handler: async (value: { ssid: string, password: string }) => {
await this.saveAndConnect(value.ssid, value.password)
},
},
],
},
})
await modal.present()
}
async presentAction (ssid: string) {
const buttons: ActionSheetButton[] = [ const buttons: ActionSheetButton[] = [
{ {
text: 'Forget', text: 'Forget',
icon: 'trash',
handler: () => { handler: () => {
this.delete(ssid) this.delete(ssid)
}, },
}, },
] ]
if (ssid !== wifi.connected) { if (ssid !== this.wifi.connected) {
buttons.unshift( buttons.unshift(
{ {
text: 'Connect', text: 'Connect',
icon: 'wifi',
handler: () => { handler: () => {
this.connect(ssid) this.connect(ssid)
}, },
@@ -47,14 +127,113 @@ export class WifiListPage {
} }
const action = await this.actionCtrl.create({ const action = await this.actionCtrl.create({
header: ssid,
subHeader: 'Manage network',
mode: 'ios',
buttons, buttons,
}) })
await action.present() await action.present()
} }
// Let's add country code here getWifiIcon (): string {
async connect (ssid: string): Promise<void> { const strength = this.wifi['signal-strength']
if (!strength) return
let path = 'assets/img/icons/wifi-'
switch (true) {
case strength > 66:
path = path + '3'
break
case strength > 33 || strength <= 66:
path = path + '2'
break
case strength < 33:
path = path + '1'
break
}
return path + '.png'
}
private async setCountry (country: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.setWifiCountry({ country })
this.wifi.country = country
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async confirmWifi (ssid: string): Promise<void> {
const timeout = 4000
const maxAttempts = 5
let attempts = 0
while (attempts < maxAttempts) {
try {
const start = new Date().valueOf()
await this.getWifi(timeout)
const end = new Date().valueOf()
if (this.wifi.connected === ssid) {
this.presentAlertSuccess(ssid)
break
} else {
attempts++
const diff = end - start
await pauseFor(Math.max(1000, timeout - diff))
if (attempts === maxAttempts) {
this.presentToastFail()
}
}
} catch (e) {
attempts++
console.error(e)
}
}
}
private async presentAlertSuccess (ssid: string): Promise<void> {
const alert = await this.alertCtrl.create({
header: `Connected to "${ssid}"`,
message: 'Note. It may take several minutes to an hour for your Embassy to reconnect over Tor.',
buttons: ['OK'],
})
await alert.present()
}
private async presentToastFail (): Promise<void> {
const toast = await this.toastCtrl.create({
header: 'Failed to connect:',
message: `Check credentials and try again`,
position: 'bottom',
duration: 4000,
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
],
cssClass: 'warning-toast',
})
await toast.present()
}
private async connect (ssid: string): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Connecting. This could take while...', message: 'Connecting. This could take while...',
@@ -63,8 +242,8 @@ export class WifiListPage {
await loader.present() await loader.present()
try { try {
await this.embassyApi.connectWifi({ ssid }) await this.api.connectWifi({ ssid })
this.wifiService.confirmWifi(ssid) await this.confirmWifi(ssid)
} catch (e) { } catch (e) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -72,7 +251,7 @@ export class WifiListPage {
} }
} }
async delete (ssid: string): Promise<void> { private async delete (ssid: string): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Deleting...', message: 'Deleting...',
@@ -81,7 +260,55 @@ export class WifiListPage {
await loader.present() await loader.present()
try { try {
await this.embassyApi.deleteWifi({ ssid }) await this.api.deleteWifi({ ssid })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async save (ssid: string, password: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.addWifi({
ssid,
password,
priority: 0,
connect: false,
})
await this.getWifi()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async saveAndConnect (ssid: string, password: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Connecting. This could take while...',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.addWifi({
ssid,
password,
priority: 0,
connect: true,
})
await this.confirmWifi(ssid)
} catch (e) { } catch (e) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -89,3 +316,26 @@ export class WifiListPage {
} }
} }
} }
const wifiSpec: ValueSpecObject = {
type: 'object',
name: 'WiFi Credentials',
description: 'Enter the network SSID and password. You can connect now or save the network for later.',
'unique-by': null,
spec: {
ssid: {
type: 'string',
name: 'Network SSID',
nullable: false,
masked: false,
copyable: false,
},
password: {
type: 'string',
name: 'Password',
nullable: false,
masked: true,
copyable: false,
},
},
}

View File

@@ -1,65 +0,0 @@
import { Injectable } from '@angular/core'
import { AlertController, ToastController } from '@ionic/angular'
import { merge, Observable, timer } from 'rxjs'
import { filter, map, take, tap } from 'rxjs/operators'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Injectable({
providedIn: 'root',
})
export class WifiService {
constructor (
private readonly toastCtrl: ToastController,
private readonly alertCtrl: AlertController,
private readonly patch: PatchDbService,
) { }
confirmWifi (ssid: string): Observable<boolean> {
const success$ = this.patch.watch$('server-info', 'wifi', 'connected')
.pipe(
filter(connected => connected === ssid),
tap(connected => this.presentAlertSuccess(connected)),
map(_ => true),
)
const timer$ = timer(20000)
.pipe(
map(_ => false),
tap(_ => this.presentToastFail()),
)
return merge(success$, timer$).pipe(take(1))
}
private async presentAlertSuccess (ssid: string): Promise<void> {
const alert = await this.alertCtrl.create({
header: `Connected to "${ssid}"`,
message: 'Note. It may take several minutes to an hour for your Embassy to reconnect over Tor.',
buttons: ['OK'],
})
await alert.present()
}
private async presentToastFail (): Promise<void> {
const toast = await this.toastCtrl.create({
header: 'Failed to connect:',
message: `Check credentials and try again`,
position: 'bottom',
duration: 4000,
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
],
cssClass: 'warning-toast',
})
await toast.present()
}
}

View File

@@ -854,6 +854,14 @@ export module Mock {
}, },
} }
export const Wifi: RR.GetWifiRes = {
ethernet: true,
ssids: ['Goosers', 'Goosers5G'],
connected: 'Goosers',
country: 'US',
'signal-strength': 50,
}
export const Disks: RR.GetDisksRes = { export const Disks: RR.GetDisksRes = {
'/dev/sda': { '/dev/sda': {
size: '32GB', size: '32GB',
@@ -1516,7 +1524,6 @@ export module Mock {
// 'tor-address': 'myveryownspecialtoraddress.onion', // 'tor-address': 'myveryownspecialtoraddress.onion',
// wifi: { // wifi: {
// ssids: ['Goosers', 'Goosers5G'], // ssids: ['Goosers', 'Goosers5G'],
// selected: 'Goosers5G',
// connected: 'Goosers5G', // connected: 'Goosers5G',
// }, // },
// 'eos-marketplace': 'https://registry.start9.com', // 'eos-marketplace': 'https://registry.start9.com',

View File

@@ -19,7 +19,7 @@ export module RR {
export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed export type LoginReq = { password: string, metadata: SessionMetadata } // auth.login - unauthed
export type loginRes = null export type loginRes = null
export type LogoutReq = {} // auth.logout export type LogoutReq = { } // auth.logout
export type LogoutRes = null export type LogoutRes = null
// server // server
@@ -30,26 +30,26 @@ export module RR {
export type GetServerLogsReq = { before?: string } // server.logs export type GetServerLogsReq = { before?: string } // server.logs
export type GetServerLogsRes = Log[] export type GetServerLogsRes = Log[]
export type GetServerMetricsReq = {} // server.metrics export type GetServerMetricsReq = { } // server.metrics
export type GetServerMetricsRes = Metrics export type GetServerMetricsRes = Metrics
export type UpdateServerReq = WithExpire<{}> // server.update export type UpdateServerReq = WithExpire<{ }> // server.update
export type UpdateServerRes = WithRevision<null> export type UpdateServerRes = WithRevision<null>
export type RestartServerReq = {} // server.restart export type RestartServerReq = { } // server.restart
export type RestartServerRes = null export type RestartServerRes = null
export type ShutdownServerReq = {} // server.shutdown export type ShutdownServerReq = { } // server.shutdown
export type ShutdownServerRes = null export type ShutdownServerRes = null
// network // network
export type RefreshLanReq = {} // network.lan.refresh export type RefreshLanReq = { } // network.lan.refresh
export type RefreshLanRes = null export type RefreshLanRes = null
// sessions // sessions
export type GetSessionsReq = {} // sessions.list export type GetSessionsReq = { } // sessions.list
export type GetSessionsRes = { export type GetSessionsRes = {
current: string, current: string,
sessions: { [hash: string]: Session } sessions: { [hash: string]: Session }
@@ -67,6 +67,7 @@ export module RR {
export type SetPackageMarketplaceRes = WithRevision<null> export type SetPackageMarketplaceRes = WithRevision<null>
// password // password
export type UpdatePasswordReq = { password: string } // password.set export type UpdatePasswordReq = { password: string } // password.set
export type UpdatePasswordRes = null export type UpdatePasswordRes = null
@@ -78,29 +79,40 @@ export module RR {
export type DeleteNotificationReq = { id: string } // notification.delete export type DeleteNotificationReq = { id: string } // notification.delete
export type DeleteNotificationRes = null export type DeleteNotificationRes = null
export type DeleteAllNotificationsReq = {} // notification.delete.all export type DeleteAllNotificationsReq = { } // notification.delete.all
export type DeleteAllNotificationsRes = null export type DeleteAllNotificationsRes = null
// wifi // wifi
export type SetWifiCountryReq = { country: string }
export type SetWifiCountryRes = null
export type GetWifiReq = { }
export type GetWifiRes = { // wifi.get
ethernet: boolean
ssids: string[]
connected: string | null
country: string | null
'signal-strength': number
}
export type AddWifiReq = { // wifi.add export type AddWifiReq = { // wifi.add
ssid: string ssid: string
password: string password: string
country: string
priority: number priority: number
connect: boolean connect: boolean
} }
export type AddWifiRes = null export type AddWifiRes = null
export type ConnectWifiReq = WithExpire<{ ssid: string }> // wifi.connect export type ConnectWifiReq = { ssid: string } // wifi.connect
export type ConnectWifiRes = WithRevision<null> export type ConnectWifiRes = null
export type DeleteWifiReq = WithExpire<{ ssid: string }> // wifi.delete export type DeleteWifiReq = { ssid: string } // wifi.delete
export type DeleteWifiRes = WithRevision<null> export type DeleteWifiRes = null
// ssh // ssh
export type GetSSHKeysReq = {} // ssh.get export type GetSSHKeysReq = { } // ssh.get
export type GetSSHKeysRes = SSHKeys export type GetSSHKeysRes = SSHKeys
export type AddSSHKeyReq = { pubkey: string } // ssh.add export type AddSSHKeyReq = { pubkey: string } // ssh.add
@@ -119,7 +131,7 @@ export module RR {
// disk // disk
export type GetDisksReq = {} // disk.list export type GetDisksReq = { } // disk.list
export type GetDisksRes = DiskInfo export type GetDisksRes = DiskInfo
export type EjectDisksReq = { logicalname: string } // disk.eject export type EjectDisksReq = { logicalname: string } // disk.eject
@@ -178,10 +190,10 @@ export module RR {
// marketplace // marketplace
export type GetMarketplaceDataReq = {} export type GetMarketplaceDataReq = { }
export type GetMarketplaceDataRes = MarketplaceData export type GetMarketplaceDataRes = MarketplaceData
export type GetMarketplaceEOSReq = {} export type GetMarketplaceEOSReq = { }
export type GetMarketplaceEOSRes = MarketplaceEOS export type GetMarketplaceEOSRes = MarketplaceEOS
export type GetMarketplacePackagesReq = { export type GetMarketplacePackagesReq = {

View File

@@ -96,17 +96,15 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
// wifi // wifi
abstract getWifi (params: RR.GetWifiReq, timeout: number): Promise<RR.GetWifiRes>
abstract setWifiCountry (params: RR.SetWifiCountryReq): Promise<RR.SetWifiCountryRes>
abstract addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> abstract addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes>
protected abstract connectWifiRaw (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> abstract connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes>
connectWifi = (params: RR.ConnectWifiReq) => this.syncResponse(
() => this.connectWifiRaw(params),
)()
protected abstract deleteWifiRaw (params: RR.DeleteWifiReq): Promise<RR.ConnectWifiRes> abstract deleteWifi (params: RR.DeleteWifiReq): Promise<RR.ConnectWifiRes>
deleteWifi = (params: RR.DeleteWifiReq) => this.syncResponse(
() => this.deleteWifiRaw(params),
)()
// ssh // ssh

View File

@@ -139,130 +139,138 @@ export class LiveApiService extends ApiService {
// notification // notification
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise < RR.GetNotificationsRes > { async getNotificationsRaw (params: RR.GetNotificationsReq): Promise <RR.GetNotificationsRes> {
return this.http.rpcRequest({ method: 'notification.list', params }) return this.http.rpcRequest({ method: 'notification.list', params })
} }
async deleteNotification (params: RR.DeleteNotificationReq): Promise < RR.DeleteNotificationRes > { async deleteNotification (params: RR.DeleteNotificationReq): Promise <RR.DeleteNotificationRes> {
return this.http.rpcRequest({ method: 'notification.delete', params }) return this.http.rpcRequest({ method: 'notification.delete', params })
} }
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise < RR.DeleteAllNotificationsRes > { async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise <RR.DeleteAllNotificationsRes> {
return this.http.rpcRequest({ method: 'notification.delete.all', params }) return this.http.rpcRequest({ method: 'notification.delete.all', params })
} }
// wifi // wifi
async addWifi (params: RR.AddWifiReq): Promise < RR.AddWifiRes > { async getWifi (params: RR.GetWifiReq, timeout?: number): Promise <RR.GetWifiRes> {
return this.http.rpcRequest({ method: 'wifi.get', params, timeout })
}
async setWifiCountry (params: RR.SetWifiCountryReq): Promise <RR.SetWifiCountryRes> {
return this.http.rpcRequest({ method: 'wifi.country.set', params })
}
async addWifi (params: RR.AddWifiReq): Promise <RR.AddWifiRes> {
return this.http.rpcRequest({ method: 'wifi.add', params }) return this.http.rpcRequest({ method: 'wifi.add', params })
} }
async connectWifiRaw (params: RR.ConnectWifiReq): Promise < RR.ConnectWifiRes > { async connectWifi (params: RR.ConnectWifiReq): Promise <RR.ConnectWifiRes> {
return this.http.rpcRequest({ method: 'wifi.connect', params }) return this.http.rpcRequest({ method: 'wifi.connect', params })
} }
async deleteWifiRaw (params: RR.DeleteWifiReq): Promise < RR.DeleteWifiRes > { async deleteWifi (params: RR.DeleteWifiReq): Promise <RR.DeleteWifiRes> {
return this.http.rpcRequest({ method: 'wifi.delete', params }) return this.http.rpcRequest({ method: 'wifi.delete', params })
} }
// ssh // ssh
async getSshKeys (params: RR.GetSSHKeysReq): Promise < RR.GetSSHKeysRes > { async getSshKeys (params: RR.GetSSHKeysReq): Promise <RR.GetSSHKeysRes> {
return this.http.rpcRequest({ method: 'ssh.get', params }) return this.http.rpcRequest({ method: 'ssh.get', params })
} }
async addSshKey (params: RR.AddSSHKeyReq): Promise < RR.AddSSHKeyRes > { async addSshKey (params: RR.AddSSHKeyReq): Promise <RR.AddSSHKeyRes> {
return this.http.rpcRequest({ method: 'ssh.add', params }) return this.http.rpcRequest({ method: 'ssh.add', params })
} }
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise < RR.DeleteSSHKeyRes > { async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise <RR.DeleteSSHKeyRes> {
return this.http.rpcRequest({ method: 'ssh.delete', params }) return this.http.rpcRequest({ method: 'ssh.delete', params })
} }
// backup // backup
async createBackupRaw (params: RR.CreateBackupReq): Promise < RR.CreateBackupRes > { async createBackupRaw (params: RR.CreateBackupReq): Promise <RR.CreateBackupRes> {
return this.http.rpcRequest({ method: 'backup.create', params }) return this.http.rpcRequest({ method: 'backup.create', params })
} }
async restoreBackupRaw (params: RR.RestoreBackupReq): Promise < RR.RestoreBackupRes > { async restoreBackupRaw (params: RR.RestoreBackupReq): Promise <RR.RestoreBackupRes> {
return this.http.rpcRequest({ method: 'backup.restore', params }) return this.http.rpcRequest({ method: 'backup.restore', params })
} }
// disk // disk
getDisks (params: RR.GetDisksReq): Promise < RR.GetDisksRes > { getDisks (params: RR.GetDisksReq): Promise <RR.GetDisksRes> {
return this.http.rpcRequest({ method: 'disk.list', params }) return this.http.rpcRequest({ method: 'disk.list', params })
} }
ejectDisk (params: RR.EjectDisksReq): Promise < RR.EjectDisksRes > { ejectDisk (params: RR.EjectDisksReq): Promise <RR.EjectDisksRes> {
return this.http.rpcRequest({ method: 'disk.eject', params }) return this.http.rpcRequest({ method: 'disk.eject', params })
} }
// package // package
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise < RR.GetPackagePropertiesRes < any > ['data'] > { async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise <RR.GetPackagePropertiesRes < any > ['data'] > {
return this.http.rpcRequest({ method: 'package.properties', params }) return this.http.rpcRequest({ method: 'package.properties', params })
.then(parsePropertiesPermissive) .then(parsePropertiesPermissive)
} }
async getPackageLogs (params: RR.GetPackageLogsReq): Promise < RR.GetPackageLogsRes > { async getPackageLogs (params: RR.GetPackageLogsReq): Promise <RR.GetPackageLogsRes> {
return this.http.rpcRequest( { method: 'package.logs', params }) return this.http.rpcRequest( { method: 'package.logs', params })
} }
async getPkgMetrics (params: RR.GetPackageMetricsReq): Promise < RR.GetPackageMetricsRes > { async getPkgMetrics (params: RR.GetPackageMetricsReq): Promise <RR.GetPackageMetricsRes> {
return this.http.rpcRequest({ method: 'package.metrics', params }) return this.http.rpcRequest({ method: 'package.metrics', params })
} }
async installPackageRaw (params: RR.InstallPackageReq): Promise < RR.InstallPackageRes > { async installPackageRaw (params: RR.InstallPackageReq): Promise <RR.InstallPackageRes> {
return this.http.rpcRequest({ method: 'package.install', params }) return this.http.rpcRequest({ method: 'package.install', params })
} }
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise < RR.DryUpdatePackageRes > { async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise <RR.DryUpdatePackageRes> {
return this.http.rpcRequest({ method: 'package.update.dry', params }) return this.http.rpcRequest({ method: 'package.update.dry', params })
} }
async getPackageConfig (params: RR.GetPackageConfigReq): Promise < RR.GetPackageConfigRes > { async getPackageConfig (params: RR.GetPackageConfigReq): Promise <RR.GetPackageConfigRes> {
return this.http.rpcRequest({ method: 'package.config.get', params }) return this.http.rpcRequest({ method: 'package.config.get', params })
} }
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise < RR.DrySetPackageConfigRes > { async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise <RR.DrySetPackageConfigRes> {
return this.http.rpcRequest({ method: 'package.config.set.dry', params }) return this.http.rpcRequest({ method: 'package.config.set.dry', params })
} }
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise < RR.SetPackageConfigRes > { async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise <RR.SetPackageConfigRes> {
return this.http.rpcRequest({ method: 'package.config.set', params }) return this.http.rpcRequest({ method: 'package.config.set', params })
} }
async restorePackageRaw (params: RR.RestorePackageReq): Promise < RR.RestorePackageRes > { async restorePackageRaw (params: RR.RestorePackageReq): Promise <RR.RestorePackageRes> {
return this.http.rpcRequest({ method: 'package.restore', params }) return this.http.rpcRequest({ method: 'package.restore', params })
} }
async executePackageAction (params: RR.ExecutePackageActionReq): Promise < RR.ExecutePackageActionRes > { async executePackageAction (params: RR.ExecutePackageActionReq): Promise <RR.ExecutePackageActionRes> {
return this.http.rpcRequest({ method: 'package.action', params }) return this.http.rpcRequest({ method: 'package.action', params })
} }
async startPackageRaw (params: RR.StartPackageReq): Promise < RR.StartPackageRes > { async startPackageRaw (params: RR.StartPackageReq): Promise <RR.StartPackageRes> {
return this.http.rpcRequest({ method: 'package.start', params }) return this.http.rpcRequest({ method: 'package.start', params })
} }
async dryStopPackage (params: RR.DryStopPackageReq): Promise < RR.DryStopPackageRes > { async dryStopPackage (params: RR.DryStopPackageReq): Promise <RR.DryStopPackageRes> {
return this.http.rpcRequest({ method: 'package.stop.dry', params }) return this.http.rpcRequest({ method: 'package.stop.dry', params })
} }
async stopPackageRaw (params: RR.StopPackageReq): Promise < RR.StopPackageRes > { async stopPackageRaw (params: RR.StopPackageReq): Promise <RR.StopPackageRes> {
return this.http.rpcRequest({ method: 'package.stop', params }) return this.http.rpcRequest({ method: 'package.stop', params })
} }
async dryRemovePackage (params: RR.DryRemovePackageReq): Promise < RR.DryRemovePackageRes > { async dryRemovePackage (params: RR.DryRemovePackageReq): Promise <RR.DryRemovePackageRes> {
return this.http.rpcRequest({ method: 'package.remove.dry', params }) return this.http.rpcRequest({ method: 'package.remove.dry', params })
} }
async removePackageRaw (params: RR.RemovePackageReq): Promise < RR.RemovePackageRes > { async removePackageRaw (params: RR.RemovePackageReq): Promise <RR.RemovePackageRes> {
return this.http.rpcRequest({ method: 'package.remove', params }) return this.http.rpcRequest({ method: 'package.remove', params })
} }
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise < RR.DryConfigureDependencyRes > { async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise <RR.DryConfigureDependencyRes> {
return this.http.rpcRequest({ method: 'package.dependency.configure.dry', params }) return this.http.rpcRequest({ method: 'package.dependency.configure.dry', params })
} }
} }

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { pauseFor } from '../../util/misc.util' import { pauseFor } from '../../util/misc.util'
import { ApiService } from './embassy-api.service' import { ApiService } from './embassy-api.service'
import { Operation, PatchOp } from 'patch-db-client' import { PatchOp } from 'patch-db-client'
import { InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model' import { InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
import { RR, WithRevision } from './api.types' import { RR, WithRevision } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { parsePropertiesPermissive } from 'src/app/util/properties.util'
@@ -223,47 +223,29 @@ export class MockApiService extends ApiService {
// wifi // wifi
async getWifi (params: RR.GetWifiReq): Promise < RR.GetWifiRes > {
await pauseFor(2000)
return Mock.Wifi
}
async setWifiCountry (params: RR.SetWifiCountryReq): Promise <RR.SetWifiCountryRes> {
await pauseFor(2000)
return null
}
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> { async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async connectWifiRaw (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> { async connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ return null
{
op: PatchOp.REPLACE,
path: '/server-info/wifi/selected',
value: params.ssid,
},
{
op: PatchOp.REPLACE,
path: '/server-info/wifi/connected',
value: params.ssid,
},
]
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
} }
async deleteWifiRaw (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> { async deleteWifi (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
await pauseFor(2000) await pauseFor(2000)
const patch: Operation[] = [ return null
{
op: PatchOp.REMOVE,
path: `/server-info/wifi/ssids/${params.ssid}`,
},
// {
// op: PatchOp.REPLACE,
// path: '/server-info/wifi/selected',
// value: null,
// },
// {
// op: PatchOp.REPLACE,
// path: '/server-info/wifi/connected',
// value: null,
// },
]
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
} }
// ssh // ssh

View File

@@ -9,9 +9,7 @@ export class Emver {
constructor () { } constructor () { }
compare (lhs: string, rhs: string): number { compare (lhs: string, rhs: string): number {
console.log('EMVER', emver)
const compare = emver.compare(lhs, rhs) const compare = emver.compare(lhs, rhs)
console.log('COMPARE', compare)
return compare return compare
} }

View File

@@ -1,14 +1,12 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms' import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'
import { By } from '@angular/platform-browser' import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecObject, ListValueSpecString, ListValueSpecUnion, UniqueBy, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecNumber, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types'
import { ConfigSpec, isValueSpecListOf, ListValueSpecNumber, ListValueSpecObject, ListValueSpecOf, ListValueSpecString, ListValueSpecUnion, UniqueBy, ValueSpec, ValueSpecEnum, ValueSpecList, ValueSpecNumber, ValueSpecObject, ValueSpecString, ValueSpecUnion } from '../pkg-config/config-types'
import { getDefaultString, Range } from '../pkg-config/config-utilities' import { getDefaultString, Range } from '../pkg-config/config-utilities'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class FormService { export class FormService {
validationMessages: { [key: string]: { type: string, message: string }[] } = { }
constructor ( constructor (
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
@@ -18,13 +16,11 @@ export class FormService {
return this.getFormGroup(config, [], current) return this.getFormGroup(config, [], current)
} }
getListItemValidators (spec: ValueSpecList, key: string, index: number) { getListItemValidators (spec: ValueSpecList) {
const listKey = `${key}/${index}`
this.validationMessages[listKey] = []
if (isValueSpecListOf(spec, 'string')) { if (isValueSpecListOf(spec, 'string')) {
return this.stringValidators(listKey, spec.spec) return this.stringValidators(spec.spec)
} else if (isValueSpecListOf(spec, 'number')) { } else if (isValueSpecListOf(spec, 'number')) {
return this.numberValidators(listKey, spec.spec) return this.numberValidators(spec.spec)
} }
} }
@@ -53,8 +49,8 @@ export class FormService {
return this.formBuilder.group(group, { validators } ) return this.formBuilder.group(group, { validators } )
} }
getListItem (key: string, index: number, spec: ValueSpecList, entry: any) { getListItem (spec: ValueSpecList, entry: any) {
const listItemValidators = this.getListItemValidators(spec, key, index) const listItemValidators = this.getListItemValidators(spec)
if (isValueSpecListOf(spec, 'string')) { if (isValueSpecListOf(spec, 'string')) {
return this.formBuilder.control(entry, listItemValidators) return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'number')) { } else if (isValueSpecListOf(spec, 'number')) {
@@ -69,12 +65,11 @@ export class FormService {
} }
private getFormEntry (key: string, spec: ValueSpec, currentValue: any): FormGroup | FormArray | FormControl { private getFormEntry (key: string, spec: ValueSpec, currentValue: any): FormGroup | FormArray | FormControl {
this.validationMessages[key] = []
let validators: ValidatorFn[] let validators: ValidatorFn[]
let value: any let value: any
switch (spec.type) { switch (spec.type) {
case 'string': case 'string':
validators = this.stringValidators(key, spec) validators = this.stringValidators(spec)
if (currentValue !== undefined) { if (currentValue !== undefined) {
value = currentValue value = currentValue
} else { } else {
@@ -82,7 +77,7 @@ export class FormService {
} }
return this.formBuilder.control(value, validators) return this.formBuilder.control(value, validators)
case 'number': case 'number':
validators = this.numberValidators(key, spec) validators = this.numberValidators(spec)
if (currentValue !== undefined) { if (currentValue !== undefined) {
value = currentValue value = currentValue
} else { } else {
@@ -92,9 +87,9 @@ export class FormService {
case 'object': case 'object':
return this.getFormGroup(spec.spec, [], currentValue) return this.getFormGroup(spec.spec, [], currentValue)
case 'list': case 'list':
validators = this.listValidators(key, spec) validators = this.listValidators(spec)
const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map((entry: any, index) => { const mapped = (Array.isArray(currentValue) ? currentValue : spec.default as any[]).map((entry: any, index) => {
return this.getListItem(key, index, spec, entry) return this.getListItem(spec, entry)
}) })
return this.formBuilder.array(mapped, validators) return this.formBuilder.array(mapped, validators)
case 'union': case 'union':
@@ -106,71 +101,43 @@ export class FormService {
} }
} }
private stringValidators (key: string, spec: ValueSpecString | ListValueSpecString): ValidatorFn[] { private stringValidators (spec: ValueSpecString | ListValueSpecString): ValidatorFn[] {
const validators: ValidatorFn[] = [] const validators: ValidatorFn[] = []
if (!(spec as ValueSpecString).nullable) { if (!(spec as ValueSpecString).nullable) {
validators.push(Validators.required) validators.push(Validators.required)
this.validationMessages[key].push({
type: 'required',
message: 'Cannot be blank.',
})
} }
if (spec.pattern) { if (spec.pattern) {
validators.push(Validators.pattern(spec.pattern)) validators.push(Validators.pattern(spec.pattern))
this.validationMessages[key].push({
type: 'pattern',
message: spec['pattern-description'],
})
} }
return validators return validators
} }
private numberValidators (key: string, spec: ValueSpecNumber | ListValueSpecNumber): ValidatorFn[] { private numberValidators (spec: ValueSpecNumber | ListValueSpecNumber): ValidatorFn[] {
const validators: ValidatorFn[] = [] const validators: ValidatorFn[] = []
if (!(spec as ValueSpecNumber).nullable) { if (!(spec as ValueSpecNumber).nullable) {
validators.push(Validators.required) validators.push(Validators.required)
this.validationMessages[key].push({
type: 'required',
message: 'Cannot be blank.',
})
} }
if (spec.integral) { if (spec.integral) {
validators.push(isInteger()) validators.push(isInteger())
this.validationMessages[key].push({
type: 'numberNotInteger',
message: 'Number must be an integer.',
})
} }
validators.push(numberInRange(spec.range)) validators.push(numberInRange(spec.range))
this.validationMessages[key].push({
type: 'numberNotInRange',
message: 'Number not in range.',
})
return validators return validators
} }
private listValidators (key: string, spec: ValueSpecList): ValidatorFn[] { private listValidators (spec: ValueSpecList): ValidatorFn[] {
const validators: ValidatorFn[] = [] const validators: ValidatorFn[] = []
validators.push(listInRange(spec.range)) validators.push(listInRange(spec.range))
this.validationMessages[key].push({
type: 'listNotInRange',
message: 'List not in range.',
})
if (!isValueSpecListOf(spec, 'enum')) { if (!isValueSpecListOf(spec, 'enum')) {
validators.push(listUnique(spec)) validators.push(listUnique(spec))
this.validationMessages[key].push({
type: 'listNotUnique',
message: 'List contains duplicate entries.',
})
} }
return validators return validators

View File

@@ -27,11 +27,12 @@ export class HttpService {
async rpcRequest<T> (rpcOpts: RPCOptions): Promise<T> { async rpcRequest<T> (rpcOpts: RPCOptions): Promise<T> {
const { url, version } = this.config.api const { url, version } = this.config.api
rpcOpts.params = rpcOpts.params || { } rpcOpts.params = rpcOpts.params || { }
const httpOpts = { const httpOpts: HttpOptions = {
method: Method.POST, method: Method.POST,
body: rpcOpts, body: rpcOpts,
url: `/${url}/${version}`, url: `/${url}/${version}`,
} }
if (rpcOpts.timeout) httpOpts.timeout = rpcOpts.timeout
const res = await this.httpRequest<RPCResponse<T>>(httpOpts) const res = await this.httpRequest<RPCResponse<T>>(httpOpts)
@@ -78,6 +79,7 @@ export class HttpService {
case Method.PATCH: req = this.http.patch(url, httpOpts.body, options) as any; break case Method.PATCH: req = this.http.patch(url, httpOpts.body, options) as any; break
case Method.DELETE: req = this.http.delete(url, options) as any; break case Method.DELETE: req = this.http.delete(url, options) as any; break
} }
console.log('REQUEST', options)
return (httpOpts.timeout ? withTimeout(req, httpOpts.timeout) : req) return (httpOpts.timeout ? withTimeout(req, httpOpts.timeout) : req)
.toPromise() .toPromise()
@@ -139,6 +141,7 @@ export interface RPCOptions {
params?: { params?: {
[param: string]: string | number | boolean | object | string[] | number[]; [param: string]: string | number | boolean | object | string[] | number[];
} }
timeout?: number
} }
interface RPCBase { interface RPCBase {

View File

@@ -19,7 +19,6 @@ export interface ServerInfo {
status: ServerStatus status: ServerStatus
'eos-marketplace': URL 'eos-marketplace': URL
'package-marketplace': URL | null // uses EOS marketplace if null 'package-marketplace': URL | null // uses EOS marketplace if null
wifi: WiFiInfo
'unread-notification-count': number 'unread-notification-count': number
specs: { specs: {
cpu: string cpu: string
@@ -38,12 +37,6 @@ export enum ServerStatus {
BackingUp = 'backing-up', BackingUp = 'backing-up',
} }
export interface WiFiInfo {
ssids: string[]
selected: string | null
connected: string | null
}
export interface PackageDataEntry { export interface PackageDataEntry {
state: PackageState state: PackageState
'static-files': { 'static-files': {

View File

@@ -44,7 +44,7 @@ export class ServerConfigService {
try { try {
await this.saveFns[key](data) await this.saveFns[key](data)
} catch (e) { } catch (e) {
this.errToast.present(e.message) this.errToast.present(e)
} finally { } finally {
loader.dismiss() loader.dismiss()
} }
@@ -84,7 +84,7 @@ export class ServerConfigService {
await alert.present() await alert.present()
} }
async presentInputModal (key: string, current?: string) { async presentModalInput (key: string, current?: string) {
const { name, description, masked } = serverConfig[key] as ValueSpecString const { name, description, masked } = serverConfig[key] as ValueSpecString
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
@@ -102,14 +102,20 @@ export class ServerConfigService {
await modal.present() await modal.present()
} }
// async presentModalForm (key: string, current?: string) { // async presentModalForm (key: string) {
// const modal = await this.modalCtrl.create({ // const modal = await this.modalCtrl.create({
// component: AppConfigValuePage, // component: AppActionInputPage,
// componentProps: { // componentProps: {
// cursor, // title: serverConfig[key].name,
// saveFn: this.saveFns[key], // spec: (serverConfig[key] as ValueSpecObject).spec,
// }, // },
// }) // })
// modal.onWillDismiss().then(res => {
// if (!res.data) return
// this.saveFns[key](res.data)
// })
// await modal.present() // await modal.present()
// } // }

View File

@@ -1,252 +1,252 @@
{ {
"AD": "Andorra", "AD": "Andorra",
"AE": "United Arab Emirates", "AE": "United Arab Emirates",
"AF": "Afghanistan", "AF": "Afghanistan",
"AG": "Antigua and Barbuda", "AG": "Antigua and Barbuda",
"AI": "Anguilla", "AI": "Anguilla",
"AL": "Albania", "AL": "Albania",
"AM": "Armenia", "AM": "Armenia",
"AO": "Angola", "AO": "Angola",
"AQ": "Antarctica", "AQ": "Antarctica",
"AR": "Argentina", "AR": "Argentina",
"AS": "American Samoa", "AS": "American Samoa",
"AT": "Austria", "AT": "Austria",
"AU": "Australia", "AU": "Australia",
"AW": "Aruba", "AW": "Aruba",
"AX": "Aland Islands", "AX": "Aland Islands",
"AZ": "Azerbaijan", "AZ": "Azerbaijan",
"BA": "Bosnia and Herzegovina", "BA": "Bosnia and Herzegovina",
"BB": "Barbados", "BB": "Barbados",
"BD": "Bangladesh", "BD": "Bangladesh",
"BE": "Belgium", "BE": "Belgium",
"BF": "Burkina Faso", "BF": "Burkina Faso",
"BG": "Bulgaria", "BG": "Bulgaria",
"BH": "Bahrain", "BH": "Bahrain",
"BI": "Burundi", "BI": "Burundi",
"BJ": "Benin", "BJ": "Benin",
"BL": "Saint Barthelemy", "BL": "Saint Barthelemy",
"BM": "Bermuda", "BM": "Bermuda",
"BN": "Brunei", "BN": "Brunei",
"BO": "Bolivia", "BO": "Bolivia",
"BQ": "Bonaire, Saint Eustatius and Saba ", "BQ": "Bonaire, Saint Eustatius and Saba ",
"BR": "Brazil", "BR": "Brazil",
"BS": "Bahamas", "BS": "Bahamas",
"BT": "Bhutan", "BT": "Bhutan",
"BV": "Bouvet Island", "BV": "Bouvet Island",
"BW": "Botswana", "BW": "Botswana",
"BY": "Belarus", "BY": "Belarus",
"BZ": "Belize", "BZ": "Belize",
"CA": "Canada", "CA": "Canada",
"CC": "Cocos Islands", "CC": "Cocos Islands",
"CD": "Democratic Republic of the Congo", "CD": "Democratic Republic of the Congo",
"CF": "Central African Republic", "CF": "Central African Republic",
"CG": "Republic of the Congo", "CG": "Republic of the Congo",
"CH": "Switzerland", "CH": "Switzerland",
"CI": "Ivory Coast", "CI": "Ivory Coast",
"CK": "Cook Islands", "CK": "Cook Islands",
"CL": "Chile", "CL": "Chile",
"CM": "Cameroon", "CM": "Cameroon",
"CN": "China", "CN": "China",
"CO": "Colombia", "CO": "Colombia",
"CR": "Costa Rica", "CR": "Costa Rica",
"CU": "Cuba", "CU": "Cuba",
"CV": "Cape Verde", "CV": "Cape Verde",
"CW": "Curacao", "CW": "Curacao",
"CX": "Christmas Island", "CX": "Christmas Island",
"CY": "Cyprus", "CY": "Cyprus",
"CZ": "Czech Republic", "CZ": "Czech Republic",
"DE": "Germany", "DE": "Germany",
"DJ": "Djibouti", "DJ": "Djibouti",
"DK": "Denmark", "DK": "Denmark",
"DM": "Dominica", "DM": "Dominica",
"DO": "Dominican Republic", "DO": "Dominican Republic",
"DZ": "Algeria", "DZ": "Algeria",
"EC": "Ecuador", "EC": "Ecuador",
"EE": "Estonia", "EE": "Estonia",
"EG": "Egypt", "EG": "Egypt",
"EH": "Western Sahara", "EH": "Western Sahara",
"ER": "Eritrea", "ER": "Eritrea",
"ES": "Spain", "ES": "Spain",
"ET": "Ethiopia", "ET": "Ethiopia",
"FI": "Finland", "FI": "Finland",
"FJ": "Fiji", "FJ": "Fiji",
"FK": "Falkland Islands", "FK": "Falkland Islands",
"FM": "Micronesia", "FM": "Micronesia",
"FO": "Faroe Islands", "FO": "Faroe Islands",
"FR": "France", "FR": "France",
"GA": "Gabon", "GA": "Gabon",
"GB": "United Kingdom", "GB": "United Kingdom",
"GD": "Grenada", "GD": "Grenada",
"GE": "Georgia", "GE": "Georgia",
"GF": "French Guiana", "GF": "French Guiana",
"GG": "Guernsey", "GG": "Guernsey",
"GH": "Ghana", "GH": "Ghana",
"GI": "Gibraltar", "GI": "Gibraltar",
"GL": "Greenland", "GL": "Greenland",
"GM": "Gambia", "GM": "Gambia",
"GN": "Guinea", "GN": "Guinea",
"GP": "Guadeloupe", "GP": "Guadeloupe",
"GQ": "Equatorial Guinea", "GQ": "Equatorial Guinea",
"GR": "Greece", "GR": "Greece",
"GS": "South Georgia and the South Sandwich Islands", "GS": "South Georgia and the South Sandwich Islands",
"GT": "Guatemala", "GT": "Guatemala",
"GU": "Guam", "GU": "Guam",
"GW": "Guinea-Bissau", "GW": "Guinea-Bissau",
"GY": "Guyana", "GY": "Guyana",
"HK": "Hong Kong", "HK": "Hong Kong",
"HM": "Heard Island and McDonald Islands", "HM": "Heard Island and McDonald Islands",
"HN": "Honduras", "HN": "Honduras",
"HR": "Croatia", "HR": "Croatia",
"HT": "Haiti", "HT": "Haiti",
"HU": "Hungary", "HU": "Hungary",
"ID": "Indonesia", "ID": "Indonesia",
"IE": "Ireland", "IE": "Ireland",
"IL": "Israel", "IL": "Israel",
"IM": "Isle of Man", "IM": "Isle of Man",
"IN": "India", "IN": "India",
"IO": "British Indian Ocean Territory", "IO": "British Indian Ocean Territory",
"IQ": "Iraq", "IQ": "Iraq",
"IR": "Iran", "IR": "Iran",
"IS": "Iceland", "IS": "Iceland",
"IT": "Italy", "IT": "Italy",
"JE": "Jersey", "JE": "Jersey",
"JM": "Jamaica", "JM": "Jamaica",
"JO": "Jordan", "JO": "Jordan",
"JP": "Japan", "JP": "Japan",
"KE": "Kenya", "KE": "Kenya",
"KG": "Kyrgyzstan", "KG": "Kyrgyzstan",
"KH": "Cambodia", "KH": "Cambodia",
"KI": "Kiribati", "KI": "Kiribati",
"KM": "Comoros", "KM": "Comoros",
"KN": "Saint Kitts and Nevis", "KN": "Saint Kitts and Nevis",
"KP": "North Korea", "KP": "North Korea",
"KR": "South Korea", "KR": "South Korea",
"KW": "Kuwait", "KW": "Kuwait",
"KY": "Cayman Islands", "KY": "Cayman Islands",
"KZ": "Kazakhstan", "KZ": "Kazakhstan",
"LA": "Laos", "LA": "Laos",
"LB": "Lebanon", "LB": "Lebanon",
"LC": "Saint Lucia", "LC": "Saint Lucia",
"LI": "Liechtenstein", "LI": "Liechtenstein",
"LK": "Sri Lanka", "LK": "Sri Lanka",
"LR": "Liberia", "LR": "Liberia",
"LS": "Lesotho", "LS": "Lesotho",
"LT": "Lithuania", "LT": "Lithuania",
"LU": "Luxembourg", "LU": "Luxembourg",
"LV": "Latvia", "LV": "Latvia",
"LY": "Libya", "LY": "Libya",
"MA": "Morocco", "MA": "Morocco",
"MC": "Monaco", "MC": "Monaco",
"MD": "Moldova", "MD": "Moldova",
"ME": "Montenegro", "ME": "Montenegro",
"MF": "Saint Martin", "MF": "Saint Martin",
"MG": "Madagascar", "MG": "Madagascar",
"MH": "Marshall Islands", "MH": "Marshall Islands",
"MK": "Macedonia", "MK": "Macedonia",
"ML": "Mali", "ML": "Mali",
"MM": "Myanmar", "MM": "Myanmar",
"MN": "Mongolia", "MN": "Mongolia",
"MO": "Macao", "MO": "Macao",
"MP": "Northern Mariana Islands", "MP": "Northern Mariana Islands",
"MQ": "Martinique", "MQ": "Martinique",
"MR": "Mauritania", "MR": "Mauritania",
"MS": "Montserrat", "MS": "Montserrat",
"MT": "Malta", "MT": "Malta",
"MU": "Mauritius", "MU": "Mauritius",
"MV": "Maldives", "MV": "Maldives",
"MW": "Malawi", "MW": "Malawi",
"MX": "Mexico", "MX": "Mexico",
"MY": "Malaysia", "MY": "Malaysia",
"MZ": "Mozambique", "MZ": "Mozambique",
"NA": "Namibia", "NA": "Namibia",
"NC": "New Caledonia", "NC": "New Caledonia",
"NE": "Niger", "NE": "Niger",
"NF": "Norfolk Island", "NF": "Norfolk Island",
"NG": "Nigeria", "NG": "Nigeria",
"NI": "Nicaragua", "NI": "Nicaragua",
"NL": "Netherlands", "NL": "Netherlands",
"NO": "Norway", "NO": "Norway",
"NP": "Nepal", "NP": "Nepal",
"NR": "Nauru", "NR": "Nauru",
"NU": "Niue", "NU": "Niue",
"NZ": "New Zealand", "NZ": "New Zealand",
"OM": "Oman", "OM": "Oman",
"PA": "Panama", "PA": "Panama",
"PE": "Peru", "PE": "Peru",
"PF": "French Polynesia", "PF": "French Polynesia",
"PG": "Papua New Guinea", "PG": "Papua New Guinea",
"PH": "Philippines", "PH": "Philippines",
"PK": "Pakistan", "PK": "Pakistan",
"PL": "Poland", "PL": "Poland",
"PM": "Saint Pierre and Miquelon", "PM": "Saint Pierre and Miquelon",
"PN": "Pitcairn", "PN": "Pitcairn",
"PR": "Puerto Rico", "PR": "Puerto Rico",
"PS": "Palestinian Territory", "PS": "Palestinian Territory",
"PT": "Portugal", "PT": "Portugal",
"PW": "Palau", "PW": "Palau",
"PY": "Paraguay", "PY": "Paraguay",
"QA": "Qatar", "QA": "Qatar",
"RE": "Reunion", "RE": "Reunion",
"RO": "Romania", "RO": "Romania",
"RS": "Serbia", "RS": "Serbia",
"RU": "Russia", "RU": "Russia",
"RW": "Rwanda", "RW": "Rwanda",
"SA": "Saudi Arabia", "SA": "Saudi Arabia",
"SB": "Solomon Islands", "SB": "Solomon Islands",
"SC": "Seychelles", "SC": "Seychelles",
"SD": "Sudan", "SD": "Sudan",
"SE": "Sweden", "SE": "Sweden",
"SG": "Singapore", "SG": "Singapore",
"SH": "Saint Helena", "SH": "Saint Helena",
"SI": "Slovenia", "SI": "Slovenia",
"SJ": "Svalbard and Jan Mayen", "SJ": "Svalbard and Jan Mayen",
"SK": "Slovakia", "SK": "Slovakia",
"SL": "Sierra Leone", "SL": "Sierra Leone",
"SM": "San Marino", "SM": "San Marino",
"SN": "Senegal", "SN": "Senegal",
"SO": "Somalia", "SO": "Somalia",
"SR": "Suriname", "SR": "Suriname",
"SS": "South Sudan", "SS": "South Sudan",
"ST": "Sao Tome and Principe", "ST": "Sao Tome and Principe",
"SV": "El Salvador", "SV": "El Salvador",
"SX": "Sint Maarten", "SX": "Sint Maarten",
"SY": "Syria", "SY": "Syria",
"SZ": "Swaziland", "SZ": "Swaziland",
"TC": "Turks and Caicos Islands", "TC": "Turks and Caicos Islands",
"TD": "Chad", "TD": "Chad",
"TF": "French Southern Territories", "TF": "French Southern Territories",
"TG": "Togo", "TG": "Togo",
"TH": "Thailand", "TH": "Thailand",
"TJ": "Tajikistan", "TJ": "Tajikistan",
"TK": "Tokelau", "TK": "Tokelau",
"TL": "East Timor", "TL": "East Timor",
"TM": "Turkmenistan", "TM": "Turkmenistan",
"TN": "Tunisia", "TN": "Tunisia",
"TO": "Tonga", "TO": "Tonga",
"TR": "Turkey", "TR": "Turkey",
"TT": "Trinidad and Tobago", "TT": "Trinidad and Tobago",
"TV": "Tuvalu", "TV": "Tuvalu",
"TW": "Taiwan", "TW": "Taiwan",
"TZ": "Tanzania", "TZ": "Tanzania",
"UA": "Ukraine", "UA": "Ukraine",
"UG": "Uganda", "UG": "Uganda",
"UM": "United States Minor Outlying Islands", "UM": "United States Minor Outlying Islands",
"US": "United States", "US": "United States",
"UY": "Uruguay", "UY": "Uruguay",
"UZ": "Uzbekistan", "UZ": "Uzbekistan",
"VA": "Vatican", "VA": "Vatican",
"VC": "Saint Vincent and the Grenadines", "VC": "Saint Vincent and the Grenadines",
"VE": "Venezuela", "VE": "Venezuela",
"VG": "British Virgin Islands", "VG": "British Virgin Islands",
"VI": "U.S. Virgin Islands", "VI": "U.S. Virgin Islands",
"VN": "Vietnam", "VN": "Vietnam",
"VU": "Vanuatu", "VU": "Vanuatu",
"WF": "Wallis and Futuna", "WF": "Wallis and Futuna",
"WS": "Samoa", "WS": "Samoa",
"XK": "Kosovo", "XK": "Kosovo",
"YE": "Yemen", "YE": "Yemen",
"YT": "Mayotte", "YT": "Mayotte",
"ZA": "South Africa", "ZA": "South Africa",
"ZM": "Zambia", "ZM": "Zambia",
"ZW": "Zimbabwe" "ZW": "Zimbabwe"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -39,6 +39,10 @@ $subheader-height: 48px;
color: var(--ion-color-warning) color: var(--ion-color-warning)
} }
.wide-alert {
--min-width: 400px;
}
.break-all { .break-all {
word-break: break-all; word-break: break-all;
} }
@@ -73,6 +77,10 @@ $subheader-height: 48px;
height: 100%; height: 100%;
} }
ion-action-sheet {
--button-color: var(--ion-color-dark) !important;
}
ion-toast { ion-toast {
--background: var(--ion-color-light); --background: var(--ion-color-light);
--button-color: var(--ion-color-dark); --button-color: var(--ion-color-dark);