refactor loaders, better err toasts, rework Embassy tab organization

This commit is contained in:
Matt Hill
2021-07-20 16:57:20 -06:00
committed by Aiden McClelland
parent 2ff9c622ac
commit eb245aea50
58 changed files with 492 additions and 610 deletions

View File

@@ -3,12 +3,11 @@ import { Storage } from '@ionic/storage'
import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/embassy/embassy-api.service'
import { Router, RoutesRecognized } from '@angular/router'
import { debounceTime, distinctUntilChanged, filter, finalize, skip, take, takeWhile } from 'rxjs/operators'
import { AlertController, IonicSafeString, ToastController } from '@ionic/angular'
import { LoaderService } from './services/loader.service'
import { debounceTime, distinctUntilChanged, filter, take, takeWhile } from 'rxjs/operators'
import { AlertController, IonicSafeString, LoadingController, ToastController } from '@ionic/angular'
import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { LoadingOptions, ToastButton } from '@ionic/core'
import { ToastButton } from '@ionic/core'
import { PatchDbService } from './services/patch-db/patch-db.service'
import { HttpService } from './services/http.service'
import { ServerStatus } from './services/patch-db/data-model'
@@ -17,6 +16,7 @@ import { StartupAlertsService } from './services/startup-alerts.service'
import { ConfigService } from './services/config.service'
import { isEmptyObject } from './util/misc.util'
import { MarketplaceApiService } from './services/api/marketplace/marketplace-api.service'
import { ErrorToastService } from './services/error-toast.service'
@Component({
selector: 'app-root',
@@ -32,7 +32,7 @@ export class AppComponent {
unreadCount: number
appPages = [
{
title: 'Installed Services',
title: 'Services',
url: '/services',
icon: 'grid-outline',
},
@@ -42,7 +42,7 @@ export class AppComponent {
icon: 'cube-outline',
},
{
title: 'Service Marketplace',
title: 'Marketplace',
url: '/marketplace',
icon: 'storefront-outline',
},
@@ -60,12 +60,13 @@ export class AppComponent {
private readonly embassyApi: ApiService,
private readonly http: HttpService,
private readonly alertCtrl: AlertController,
private readonly loader: LoaderService,
private readonly emver: Emver,
private readonly connectionService: ConnectionService,
private readonly marketplaceApi: MarketplaceApiService,
private readonly startupAlertsService: StartupAlertsService,
private readonly toastCtrl: ToastController,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly patch: PatchDbService,
private readonly config: ConfigService,
readonly splitPane: SplitPaneTracker,
@@ -198,8 +199,8 @@ export class AppComponent {
.subscribe(status => {
const maintenance = '/maintenance'
const route = this.router.url
console.log('STATUS', status, 'URL', route)
if (status === ServerStatus.Running && route.startsWith(maintenance)) {
this.showMenu = true
this.router.navigate([''], { replaceUrl: true })
}
if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(status) && !route.startsWith(maintenance)) {
@@ -215,6 +216,7 @@ export class AppComponent {
takeWhile(() => auth === AuthState.VERIFIED),
)
.subscribe(version => {
console.log('VERSIONS', this.config.version, version)
if (this.emver.compare(this.config.version, version) !== 0) {
this.presentAlertRefreshNeeded()
}
@@ -275,10 +277,21 @@ export class AppComponent {
}
private async logout () {
this.loader.of(LoadingSpinner('Logging out...'))
.displayDuringP(this.embassyApi.logout({ }))
.then(() => this.authService.setUnverified())
.catch(e => this.setError(e))
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Logging out...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.logout({ })
this.authService.setUnverified()
} catch (e) {
await this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async presentToastNotifications () {
@@ -347,35 +360,7 @@ export class AppComponent {
await this.offlineToast.present()
}
private async setError (e: Error) {
console.error(e)
await this.presentError(e.message)
}
private async presentError (e: string) {
const alert = await this.alertCtrl.create({
backdropDismiss: true,
message: `Exception on logout: ${e}`,
buttons: [
{
text: 'Dismiss',
role: 'cancel',
},
],
})
await alert.present()
}
splitPaneVisible (e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
}
const LoadingSpinner: (m?: string) => LoadingOptions = (m) => {
const toMergeIn = m ? { message: m } : { }
return {
spinner: 'lines',
cssClass: 'loader',
...toMergeIn,
} as LoadingOptions
}

View File

@@ -1,9 +1,8 @@
import { Component, Input, OnInit } from '@angular/core'
import { BehaviorSubject, from, Subject } from 'rxjs'
import { takeUntil } from 'rxjs/operators'
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { Loadable } from '../loadable'
import { Loadable, markAsLoadingDuring$ } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({

View File

@@ -2,9 +2,8 @@ import { Component, Input, OnInit } from '@angular/core'
import { BehaviorSubject, from, Subject } from 'rxjs'
import { takeUntil, tap } from 'rxjs/operators'
import { Breakages } from 'src/app/services/api/api.types'
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { capitalizeFirstLetter, isEmptyObject } from 'src/app/util/misc.util'
import { Loadable } from '../loadable'
import { Loadable, markAsLoadingDuring$ } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({

View File

@@ -1,4 +1,6 @@
import { BehaviorSubject, Subject } from 'rxjs'
import { BehaviorSubject, Observable, Subject } from 'rxjs'
import { concatMap, finalize } from 'rxjs/operators'
import { fromSync$, emitAfter$ } from 'src/app/util/rxjs.util'
export interface Loadable {
load: (prevResult?: any) => void
@@ -7,3 +9,16 @@ export interface Loadable {
cancel$: Subject<void> // will cancel load function
}
export function markAsLoadingDuring$<T> ($trigger$: Subject<boolean>, o: Observable<T>): Observable<T> {
let shouldBeOn = true
const displayIfItsBeenAtLeast = 5 // ms
return fromSync$(() => {
emitAfter$(displayIfItsBeenAtLeast).subscribe(() => { if (shouldBeOn) $trigger$.next(true) })
}).pipe(
concatMap(() => o),
finalize(() => {
$trigger$.next(false)
shouldBeOn = false
}),
)
}

View File

@@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
import { LoaderService } from 'src/app/services/loader.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { Action } from 'src/app/services/patch-db/data-model'
@Component({
@@ -20,8 +20,8 @@ export class AppActionInputPage {
constructor (
private readonly modalCtrl: ModalController,
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
private loaderService: LoaderService,
) { }
ngOnInit () {
@@ -35,18 +35,21 @@ export class AppActionInputPage {
}
async save (): Promise<void> {
this.loaderService.of({
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Executing action',
cssClass: 'loader-ontop-of-all',
}).displayDuringAsync(async () => {
try {
await this.execute()
this.modalCtrl.dismiss()
} catch (e) {
this.error = e.message
}
})
await loader.present()
try {
await this.execute()
this.modalCtrl.dismiss()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
handleObjectEdit (): void {

View File

@@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core'
import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/pkg-config/config-utilities'
import { AlertController, ModalController, ToastController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { AlertController, LoadingController, ModalController, ToastController } from '@ionic/angular'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ValueSpecOf } from 'src/app/pkg-config/config-types'
import { copyToClipboard } from 'src/app/util/web.util'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'app-config-value',
@@ -29,10 +29,11 @@ export class AppConfigValuePage {
rangeDescription: string
constructor (
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly toastCtrl: ToastController,
private readonly errToast: ErrorToastService,
) { }
ngOnInit () {
@@ -68,14 +69,21 @@ export class AppConfigValuePage {
this.value = Number(this.value)
}
this.loader.displayDuringP(
this.saveFn(this.value).catch(e => {
console.error(e)
this.error = e.message
}),
)
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
})
await loader.present()
await this.modalCtrl.dismiss(this.value)
try {
await this.saveFn(this.value)
this.modalCtrl.dismiss(this.value)
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
refreshDefault () {

View File

@@ -20,10 +20,10 @@
</div>
<div style="display: flex; justify-content: flex-end; align-items: center;">
<ion-button fill="clear" color="medium" (click)="cancel()">
<ion-button fill="clear" (click)="cancel()">
Cancel
</ion-button>
<ion-button fill="clear" (click)="submit()">
<ion-button fill="clear" (click)="submit()" [disabled]="!password.length">
{{ type === 'backup' ? 'Create Backup' : 'Restore Backup' }}
</ion-button>
</div>

View File

@@ -24,8 +24,7 @@ export class MarkdownPage {
try {
this.content = await this.embassyApi.getStatic(this.contentUrl)
} catch (e) {
console.error(e.message)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
this.loading = false
}

View File

@@ -1,16 +1,15 @@
<ion-header>
<ion-toolbar>
<ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to {{ version }}!</ion-label>
</ion-title>
<ion-title>Welcome to {{ version }}!</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<h2>Highlights</h2>
<h2>A Whole New Embassy</h2>
<div class="main-content">
<p>This release fixes a bug with certificate generation that caused the Embassy web interface to become inaccessible</p>
<p>Version {{ version }} is something new.</p>
<p>This release also enables displaying Service license information and contains utilities to facilitate the next major release of EmbassyOS.</p>
</div>
<div class="close-button">

View File

@@ -1,7 +1,5 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'os-welcome',
@@ -13,16 +11,9 @@ export class OSWelcomePage {
constructor (
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
private readonly config: ConfigService,
) { }
async dismiss () {
this.embassyApi.setDbValue({ pointer: '/welcome-ack', value: this.config.version })
.catch(console.error)
// return false to skip subsequent alert modals (e.g. check for updates modals)
// return true to show subsequent alert modals
return this.modalCtrl.dismiss(true)
return this.modalCtrl.dismiss()
}
}

View File

@@ -1,17 +1,15 @@
import { Component, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { AlertController, IonContent, ModalController, NavController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { HttpErrorResponse } from '@angular/common/http'
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Action, Manifest, PackageDataEntry, PackageMainStatus } from 'src/app/services/patch-db/data-model'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { Subscription } from 'rxjs'
import { AppConfigObjectPage } from 'src/app/modals/app-config-object/app-config-object.page'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { AppActionInputPage } from 'src/app/modals/app-action-input/app-action-input.page'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'app-actions',
@@ -29,7 +27,8 @@ export class AppActionsPage {
private readonly embassyApi: ApiService,
private readonly modalCtrl: ModalController,
private readonly alertCtrl: AlertController,
private readonly loaderService: LoaderService,
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
private readonly wizardBaker: WizardBaker,
private readonly navCtrl: NavController,
public readonly patch: PatchDbService,
@@ -120,11 +119,16 @@ export class AppActionsPage {
return this.navCtrl.navigateRoot('/services')
}
private async executeAction (pkgId: string, actionId: string) {
private async executeAction (pkgId: string, actionId: string): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Executing action...',
cssClass: 'loader',
})
await loader.present()
try {
const res = await this.loaderService.displayDuringP(
this.embassyApi.executePackageAction({ id: pkgId, 'action-id': actionId }),
)
const res = await this.embassyApi.executePackageAction({ id: pkgId, 'action-id': actionId })
const successAlert = await this.alertCtrl.create({
header: 'Execution Complete',
@@ -132,23 +136,11 @@ export class AppActionsPage {
buttons: ['OK'],
cssClass: 'alert-success-message',
})
return await successAlert.present()
await successAlert.present()
} catch (e) {
if (e instanceof HttpErrorResponse) {
this.presentAlertActionFail(e.status, e.message)
} else {
this.presentAlertActionFail(-1, e.message || JSON.stringify(e))
}
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async presentAlertActionFail (code: number, message: string): Promise<void> {
const failureAlert = await this.alertCtrl.create({
header: 'Execution Failed',
message: `Error code ${code}. ${message}`,
buttons: ['OK'],
cssClass: 'alert-error-message',
})
return await failureAlert.present()
}
}

View File

@@ -1,9 +1,8 @@
import { Component, ViewChild } from '@angular/core'
import { NavController, AlertController, ModalController, IonContent } from '@ionic/angular'
import { NavController, AlertController, ModalController, IonContent, LoadingController } from '@ionic/angular'
import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { isEmptyObject, Recommendation } from 'src/app/util/misc.util'
import { LoaderService } from 'src/app/services/loader.service'
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
import { from, fromEvent, of, Subscription } from 'rxjs'
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
@@ -13,6 +12,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'app-config',
@@ -51,7 +51,8 @@ export class AppConfigPage {
private readonly route: ActivatedRoute,
private readonly wizardBaker: WizardBaker,
private readonly embassyApi: ApiService,
private readonly loader: LoaderService,
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
private readonly alertCtrl: AlertController,
private readonly modalController: ModalController,
private readonly trackingModalCtrl: TrackingModalController,
@@ -85,7 +86,7 @@ export class AppConfigPage {
this.patch.watch$('package-data', pkgId)
.pipe(
tap(pkg => this.pkg = pkg),
tap(() => this.loadingText = 'Fetching config spec...'),
tap(() => this.loadingText = 'Loading config...'),
concatMap(() => this.embassyApi.getPackageConfig({ id: pkgId })),
concatMap(({ spec, config }) => {
const rec = history.state && history.state.configRecommendation as Recommendation
@@ -157,11 +158,14 @@ export class AppConfigPage {
}
async save (pkg: PackageDataEntry) {
return this.loader.of({
message: `Saving config...`,
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: `Saving config...`,
cssClass: 'loader',
}).displayDuringAsync(async () => {
})
await loader.present()
try {
const breakages = await this.embassyApi.drySetPackageConfig({ id: pkg.manifest.id, config: this.config })
if (!isEmptyObject(breakages.length)) {
@@ -172,17 +176,16 @@ export class AppConfigPage {
breakages,
}),
)
if (cancelled) return { skip: true }
if (cancelled) return
}
return this.embassyApi.setPackageConfig({ id: pkg.manifest.id, config: this.config })
.then(() => ({ skip: false }))
})
.then(({ skip }) => {
if (skip) return
await this.embassyApi.setPackageConfig({ id: pkg.manifest.id, config: this.config })
this.navCtrl.back()
})
.catch(e => this.error = { text: e.message })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
handleObjectEdit () {

View File

@@ -31,8 +31,7 @@ export class AppInstructionsPage {
try {
this.instructions = await this.embassyApi.getStatic(url)
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
this.loading = false
}

View File

@@ -33,8 +33,7 @@ export class AppLogsPage {
this.logs = logs.map(l => `${l.timestamp} ${l.log}`).join('\n\n')
setTimeout(async () => await this.content.scrollToBottom(100), 200)
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
}
}
}

View File

@@ -68,8 +68,7 @@ export class AppMetricsPage {
try {
this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId})
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
this.stopDaemon()
} finally {
this.loading = false

View File

@@ -122,8 +122,7 @@ export class AppPropertiesPage {
this.properties = await this.embassyApi.getPackageProperties({ id: this.pkgId })
this.node = JsonPointer.get(this.properties, this.pointer || '')
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
this.loading = false
}

View File

@@ -32,36 +32,29 @@
<ion-text class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you intend to restore.</ion-text>
</ion-item>
<ion-grid>
<ion-row>
<ion-col *ngFor="let disk of disks | keyvalue" sizeSm="12" sizeMd="6">
<ion-card>
<ion-card-header>
<ion-card-title>
{{ disk.value.size }}
</ion-card-title>
<ion-card-subtitle>
{{ disk.key }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-item-group>
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
<ng-template #unavailable>
<p><ion-text color="danger">Unavailable</ion-text></p>
</ng-template>
</ion-label>
</ion-item>
</ion-item-group>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
<ion-card *ngFor="let disk of disks | keyvalue">
<ion-card-header>
<ion-card-title>
{{ disk.value.size }}
</ion-card-title>
<ion-card-subtitle>
{{ disk.key }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-item-group>
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
<ng-template #unavailable>
<p><ion-text color="danger">Unavailable</ion-text></p>
</ng-template>
</ion-label>
</ion-item>
</ion-item-group>
</ion-card-content>
</ion-card>
</ng-template>
</ion-content>

View File

@@ -7,6 +7,7 @@ import { ActivatedRoute } from '@angular/router'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Subscription } from 'rxjs'
import { take } from 'rxjs/operators'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'app-restore',
@@ -18,7 +19,6 @@ export class AppRestorePage {
pkgId: string
title: string
loading = true
error: string
allPartitionsMounted: boolean
@ViewChild(IonContent) content: IonContent
@@ -29,6 +29,7 @@ export class AppRestorePage {
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
public readonly patch: PatchDbService,
) { }
@@ -51,8 +52,7 @@ export class AppRestorePage {
this.disks = await this.embassyApi.getDisks({ })
this.allPartitionsMounted = Object.values(this.disks).every(d => Object.values(d.partitions).every(p => p['is-mounted']))
} catch (e) {
console.error(e)
this.error = e.message
this.errToast.present(e)
} finally {
this.loading = false
}
@@ -74,12 +74,10 @@ export class AppRestorePage {
this.restore(logicalname, data.password)
})
return await m.present()
await m.present()
}
private async restore (logicalname: string, password: string): Promise<void> {
this.error = ''
const loader = await this.loadingCtrl.create({
spinner: 'lines',
})
@@ -92,8 +90,7 @@ export class AppRestorePage {
password,
})
} catch (e) {
console.error(e)
this.error = e.message
this.errToast.present(e)
} finally {
loader.dismiss()
}

View File

@@ -1,10 +1,9 @@
import { Component, ViewChild } from '@angular/core'
import { AlertController, NavController, ModalController, IonContent } from '@ionic/angular'
import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { ActivatedRoute, NavigationExtras } from '@angular/router'
import { chill, isEmptyObject, Recommendation } from 'src/app/util/misc.util'
import { LoaderService } from 'src/app/services/loader.service'
import { combineLatest, Observable, of, Subscription } from 'rxjs'
import { combineLatest, Subscription } from 'rxjs'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { ConfigService } from 'src/app/services/config.service'
@@ -38,7 +37,7 @@ export class AppShowPage {
private readonly route: ActivatedRoute,
private readonly navCtrl: NavController,
private readonly errToast: ErrorToastService,
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
private readonly wizardBaker: WizardBaker,
@@ -77,11 +76,14 @@ export class AppShowPage {
async stop (): Promise<void> {
const { id, title, version } = this.pkg.manifest
await this.loader.of({
const loader = await this.loadingCtrl.create({
message: `Stopping...`,
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync(async () => {
})
await loader.present()
try {
const breakages = await this.embassyApi.dryStopPackage({ id })
console.log('BREAKAGES', breakages)
@@ -96,12 +98,14 @@ export class AppShowPage {
breakages,
}),
)
if (cancelled) return { }
if (cancelled) return
}
return this.embassyApi.stopPackage({ id }).then(chill)
}).catch(e => this.setError(e))
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async tryStart (): Promise<void> {
@@ -206,19 +210,20 @@ export class AppShowPage {
}
private async start (): Promise<void> {
this.loader.of({
const loader = await this.loadingCtrl.create({
message: `Starting...`,
spinner: 'lines',
cssClass: 'loader',
}).displayDuringP(
this.embassyApi.startPackage({ id: this.pkgId }),
).catch(e => this.setError(e))
}
})
await loader.present()
private setError (e: Error): Observable<void> {
console.error(e)
this.errToast.present(e.message)
return of()
try {
await this.embassyApi.startPackage({ id: this.pkgId })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
setButtons (): void {

View File

@@ -68,8 +68,7 @@ export class MarketplaceListPage {
this.data.categories = [this.category, 'updates'].concat(filterdCategories).concat(['all'])
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
this.pageLoading = false
this.pkgsLoading = false
@@ -128,8 +127,7 @@ export class MarketplaceListPage {
this.pkgs = doInfinite ? this.pkgs.concat(pkgs) : pkgs
}
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
this.pkgsLoading = false
}

View File

@@ -71,8 +71,7 @@ export class MarketplaceShowPage {
try {
await this.marketplaceService.getPkg(this.pkgId, version)
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
await pauseFor(100)
this.loading = false

View File

@@ -1,8 +1,7 @@
import { Component } from '@angular/core'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ServerNotification, ServerNotifications } from 'src/app/services/api/api.types'
import { AlertController } from '@ionic/angular'
import { AlertController, LoadingController } from '@ionic/angular'
import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@@ -21,7 +20,7 @@ export class NotificationsPage {
constructor (
private readonly embassyApi: ApiService,
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
private readonly route: ActivatedRoute,
@@ -52,26 +51,28 @@ export class NotificationsPage {
this.needInfinite = notifications.length >= this.perPage
this.page++
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
return notifications
}
}
async remove (id: string, index: number): Promise<void> {
this.loader.of({
message: 'Deleting...',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Deleting...',
cssClass: 'loader',
}).displayDuringP(
this.embassyApi.deleteNotification({ id }).then(() => {
this.notifications.splice(index, 1)
}),
).catch(e => {
console.error(e)
this.errToast.present(e.message)
})
await loader.present()
try {
await this.embassyApi.deleteNotification({ id })
this.notifications.splice(index, 1)
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async viewBackupReport (notification: ServerNotification<1>) {

View File

@@ -1,18 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Developer Options</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top" *ngIf="patch.data['server-info'] as server">
<ion-item-group>
<ion-item detail="true" button [routerLink]="['ssh-keys']">
<ion-label>SSH Keys</ion-label>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -1,20 +0,0 @@
import { Component } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({
selector: 'dev-options',
templateUrl: './dev-options.page.html',
styleUrls: ['./dev-options.page.scss'],
})
export class DevOptionsPage {
constructor (
private readonly serverConfigService: ServerConfigService,
public readonly patch: PatchDbService,
) { }
async presentModalValueEdit (key: string, current?: any): Promise<void> {
await this.serverConfigService.presentModalValueEdit(key, current)
}
}

View File

@@ -21,6 +21,10 @@
<ion-icon slot="start" name="list-outline"></ion-icon>
<ion-label>View Instructions</ion-label>
</ion-item>
<ion-item button (click)="installCert()" [disabled]="lanDisabled">
<ion-icon slot="start" name="download-outline"></ion-icon>
<ion-label>Download Root Certificate Authority</ion-label>
</ion-item>
<ng-container *ngIf="lanDisabled">
<ion-item-divider></ion-item-divider>
@@ -37,38 +41,13 @@
<ion-item>
<ion-label class="ion-text-wrap">
<p style="padding-bottom: 6px;">Troubleshooting</p>
<h2>If you are having issues connecting to your Embassy over LAN, try refreshing the network by clicking the button below.</h2>
<h2>If you are having issues connecting to your Embassy over LAN, try refreshing your LAN services by clicking the button below.</h2>
</ion-label>
</ion-item>
<ion-item button (click)="refreshLAN()" detail="false">
<ion-icon slot="start" name="refresh-outline"></ion-icon>
<ion-label>Refresh Network</ion-label>
<ion-label>Refresh LAN</ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<!-- Certificate and Lan Address -->
<ng-container *ngIf="!lanDisabled">
<ion-item>
<ion-label class="ion-text-wrap">
<h2>Root Certificate Authority</h2>
<p>Embassy Local CA</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="installCert()">
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
</ion-button>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>{{ lanAddress }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copyLAN()">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ion-item-group>
<!-- hidden element for downloading cert -->

View File

@@ -1,11 +1,11 @@
import { Component } from '@angular/core'
import { isPlatform, ToastController } from '@ionic/angular'
import { isPlatform, LoadingController, ToastController } from '@ionic/angular'
import { copyToClipboard } from 'src/app/util/web.util'
import { ConfigService } from 'src/app/services/config.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Subscription } from 'rxjs'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'lan',
@@ -25,7 +25,8 @@ export class LANPage {
constructor (
private readonly toastCtrl: ToastController,
private readonly config: ConfigService,
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly patch: PatchDbService,
) { }
@@ -49,15 +50,21 @@ export class LANPage {
}
async refreshLAN (): Promise<void> {
this.loader.of({
message: 'Refreshing Network',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Refreshing LAN...',
cssClass: 'loader',
}).displayDuringAsync( async () => {
await this.embassyApi.refreshLan({ })
}).catch(e => {
console.error(e)
})
await loader.present()
try {
await this.embassyApi.refreshLan({ })
this.presentToastSuccess()
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async copyLAN (): Promise <void> {
@@ -74,6 +81,27 @@ export class LANPage {
installCert (): void {
document.getElementById('install-cert').click()
}
private async presentToastSuccess (): Promise<void> {
const toast = await this.toastCtrl.create({
header: 'Success',
message: `LAN refreshed.`,
position: 'bottom',
duration: 3000,
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
],
cssClass: 'success-toast',
})
await toast.present()
}
}
enum LanSetupIssue {

View File

@@ -1,28 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { PrivacyPage } from './privacy.page'
import { Routes, RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
const routes: Routes = [
{
path: '',
component: PrivacyPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
SharingModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
],
declarations: [
PrivacyPage,
],
})
export class PrivacyPageModule { }

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { DevOptionsPage } from './dev-options.page'
import { SecurityOptionsPage } from './security-options.page'
import { Routes, RouterModule } from '@angular/router'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
@@ -10,7 +10,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: DevOptionsPage,
component: SecurityOptionsPage,
},
]
@@ -24,7 +24,7 @@ const routes: Routes = [
SharingModule,
],
declarations: [
DevOptionsPage,
SecurityOptionsPage,
],
})
export class DevOptionsPageModule { }
export class SecurityOptionsPageModule { }

View File

@@ -7,28 +7,37 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-content class="ion-padding-top" *ngIf="patch.data['server-info'] as server">
<ion-item-group>
<ion-item-divider>Marketplace Settings</ion-item-divider>
<ion-item button (click)="presentModalValueEdit('eosMarketplace', patch.data['server-info']['eos-marketplace'])">
<ion-label>Use Tor</ion-label>
<ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalValueEdit('shareStats', patch.data['server-info']['share-stats'])">
<ion-label>Share Anonymous Statistics</ion-label>
<ion-note slot="end">{{ patch.data['server-info']['share-stats'] }}</ion-note>
</ion-item>
<ion-item-divider>Marketplace</ion-item-divider>
<ion-item button (click)="presentModalValueEdit('autoCheckUpdates', patch.data.ui['auto-check-updates'])">
<ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] }}</ion-note>
</ion-item>
<ion-item button (click)="presentModalValueEdit('eosMarketplace', patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor)">
<ion-label>Tor Only Marketplace</ion-label>
<ion-note slot="end">{{ patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor }}</ion-note>
</ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', patch.data['server-info']['package-marketplace'])">
<ion-label>Package Marketplace</ion-label>
<ion-note slot="end">{{ patch.data['server-info']['package-marketplace'] }}</ion-note>
</ion-item> -->
<ion-item button (click)="presentModalValueEdit('autoCheckUpdates', patch.data.ui['auto-check-updates'])">
<ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] }}</ion-note>
</ion-item>
<!-- <ion-item-divider></ion-item-divider>
<ion-item button (click)="presentModalValueEdit('password')">
<ion-item-divider>Security</ion-item-divider>
<!-- <ion-item button (click)="presentModalValueEdit('password')">
<ion-label>Change Password</ion-label>
<ion-note slot="end">********</ion-note>
</ion-item> -->
<ion-item detail="true" button [routerLink]="['ssh-keys']">
<ion-label>SSH Keys</ion-label>
</ion-item>
</ion-item-group>
</ion-content>
</ion-content>

View File

@@ -1,16 +1,14 @@
import { Component } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { Subscription } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'privacy',
templateUrl: './privacy.page.html',
styleUrls: ['./privacy.page.scss'],
selector: 'security-options',
templateUrl: './security-options.page.html',
styleUrls: ['./security-options.page.scss'],
})
export class PrivacyPage {
subs: Subscription[] = []
export class SecurityOptionsPage {
constructor (
private readonly serverConfigService: ServerConfigService,
@@ -18,7 +16,7 @@ export class PrivacyPage {
public readonly patch: PatchDbService,
) { }
async presentModalValueEdit (key: string, current?: string): Promise<void> {
async presentModalValueEdit (key: string, current?: any): Promise<void> {
await this.serverConfigService.presentModalValueEdit(key, current)
}
}

View File

@@ -4,11 +4,11 @@ import { Routes, RouterModule } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () => import('./dev-options/dev-options.module').then(m => m.DevOptionsPageModule),
loadChildren: () => import('./security-options/security-options.module').then(m => m.SecurityOptionsPageModule),
},
{
path: 'ssh-keys',
loadChildren: () => import('./dev-ssh-keys/dev-ssh-keys.module').then(m => m.DevSSHKeysPageModule),
loadChildren: () => import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule),
},
]
@@ -16,4 +16,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DeveloperRoutingModule { }
export class SecurityRoutingModule { }

View File

@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevSSHKeysPage } from './dev-ssh-keys.page'
import { SSHKeysPage } from './ssh-keys.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
@@ -10,7 +10,7 @@ import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text
const routes: Routes = [
{
path: '',
component: DevSSHKeysPage,
component: SSHKeysPage,
},
]
@@ -23,6 +23,6 @@ const routes: Routes = [
SharingModule,
TextSpinnerComponentModule,
],
declarations: [DevSSHKeysPage],
declarations: [SSHKeysPage],
})
export class DevSSHKeysPageModule { }
export class SSHKeysPageModule { }

View File

@@ -1,24 +1,23 @@
import { Component } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { AlertController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { AlertController, LoadingController } from '@ionic/angular'
import { SSHService } from './ssh.service'
import { Subscription } from 'rxjs'
import { SSHKeys } from 'src/app/services/api/api.types'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'dev-ssh-keys',
templateUrl: 'dev-ssh-keys.page.html',
styleUrls: ['dev-ssh-keys.page.scss'],
selector: 'ssh-keys',
templateUrl: 'ssh-keys.page.html',
styleUrls: ['ssh-keys.page.scss'],
})
export class DevSSHKeysPage {
export class SSHKeysPage {
loading = true
sshKeys: SSHKeys
subs: Subscription[] = []
constructor (
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly serverConfigService: ServerConfigService,
private readonly alertCtrl: AlertController,
@@ -69,16 +68,20 @@ export class DevSSHKeysPage {
}
async delete (hash: string): Promise<void> {
this.loader.of({
message: 'Deleting...',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Deleting...',
cssClass: 'loader',
}).displayDuringAsync(async () => {
await this.sshService.delete(hash)
}).catch(e => {
console.error(e)
this.errToast.present(e.message)
})
await loader.present()
try {
await this.sshService.delete(hash)
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
asIsOrder (a: any, b: any) {

View File

@@ -33,36 +33,30 @@
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you wish to restore.</ion-text>
</ion-item>
<ion-grid>
<ion-row>
<ion-col *ngFor="let disk of disks | keyvalue" sizeSm="12" sizeMd="6">
<ion-card>
<ion-card-header>
<ion-card-title>
{{ disk.value.size }}
</ion-card-title>
<ion-card-subtitle>
{{ disk.key }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-item-group>
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
<ng-template #unavailable>
<p><ion-text color="danger">Unavailable</ion-text></p>
</ng-template>
</ion-label>
</ion-item>
</ion-item-group>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
<ion-card *ngFor="let disk of disks | keyvalue">
<ion-card-header>
<ion-card-title>
{{ disk.value.size }}
</ion-card-title>
<ion-card-subtitle>
{{ disk.key }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-item-group>
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key)">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
<ng-template #unavailable>
<p><ion-text color="danger">Unavailable</ion-text></p>
</ng-template>
</ion-label>
</ion-item>
</ion-item-group>
</ion-card-content>
</ion-card>
</ng-template>
</ion-content>

View File

@@ -3,6 +3,7 @@ import { LoadingController, ModalController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
import { DiskInfo } from 'src/app/services/api/api.types'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'server-backup',
@@ -12,12 +13,12 @@ import { DiskInfo } from 'src/app/services/api/api.types'
export class ServerBackupPage {
disks: DiskInfo
loading = true
error: string
allPartitionsMounted: boolean
constructor (
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
private readonly errToast: ErrorToastService,
private readonly loadingCtrl: LoadingController,
) { }
@@ -35,8 +36,7 @@ export class ServerBackupPage {
this.disks = await this.embassyApi.getDisks({ })
this.allPartitionsMounted = Object.values(this.disks).every(d => Object.values(d.partitions).every(p => p['is-mounted']))
} catch (e) {
console.error(e)
this.error = e.message
this.errToast.present(e)
} finally {
this.loading = false
}
@@ -62,18 +62,17 @@ export class ServerBackupPage {
}
private async create (logicalname: string, password: string): Promise<void> {
this.error = ''
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Starting backup...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.createBackup({ logicalname, password })
} catch (e) {
console.error(e)
this.error = e.message
this.errToast.present(e)
} finally {
loader.dismiss()
}

View File

@@ -30,8 +30,7 @@ export class ServerLogsPage {
this.logs = logs.map(l => `${l.timestamp} ${l.log}`).join('\n\n')
setTimeout(async () => await this.content.scrollToBottom(100), 200)
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
} finally {
this.loading = false
}

View File

@@ -43,8 +43,7 @@ export class ServerMetricsPage {
try {
this.metrics = await this.embassyApi.getServerMetrics({ })
} catch (e) {
console.error(e)
this.errToast.present(e.message)
this.errToast.present(e)
this.stopDaemon()
} finally {
this.loading = false

View File

@@ -22,10 +22,6 @@ const routes: Routes = [
path: 'logs',
loadChildren: () => import('./server-logs/server-logs.module').then(m => m.ServerLogsPageModule),
},
{
path: 'privacy',
loadChildren: () => import('./privacy/privacy.module').then(m => m.PrivacyPageModule),
},
{
path: 'wifi',
loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiListPageModule),
@@ -35,8 +31,8 @@ const routes: Routes = [
loadChildren: () => import('./lan/lan.module').then(m => m.LANPageModule),
},
{
path: 'developer',
loadChildren: () => import('./developer-routes/developer-routing.module').then( m => m.DeveloperRoutingModule),
path: 'security',
loadChildren: () => import('./security-routes/security-routing.module').then( m => m.SecurityRoutingModule),
},
]

View File

@@ -9,15 +9,13 @@
<ion-content>
<ion-grid>
<ion-row>
<ion-col *ngFor="let cat of settings | keyvalue : asIsOrder" sizeXs="12" sizeMd="6">
<ion-item-divider>{{ cat.key }}</ion-item-divider>
<ion-item style="cursor: pointer;" button *ngFor="let button of cat.value" (click)="button.action()">
<ion-icon slot="start" [name]="button.icon"></ion-icon>
<ion-label>{{ button.title }}</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
<ion-item-group>
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
<ion-item-divider>{{ cat.key }}</ion-item-divider>
<ion-item style="cursor: pointer;" button *ngFor="let button of cat.value" (click)="button.action()">
<ion-icon slot="start" [name]="button.icon"></ion-icon>
<ion-label>{{ button.title }}</ion-label>
</ion-item>
</div>
</ion-item-group>
</ion-content>

View File

@@ -1,9 +1,8 @@
import { Component } from '@angular/core'
import { LoadingOptions } from '@ionic/core'
import { AlertController, NavController } from '@ionic/angular'
import { AlertController, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ActivatedRoute } from '@angular/router'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
selector: 'server-show',
@@ -15,7 +14,8 @@ export class ServerShowPage {
constructor (
private readonly alertCtrl: AlertController,
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
@@ -70,21 +70,37 @@ export class ServerShowPage {
}
private async restart () {
this.loader
.of(LoadingSpinner(`Restarting...`))
.displayDuringAsync( async () => {
await this.embassyApi.restartServer({ })
})
.catch(console.error)
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Restarting...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.restartServer({ })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async shutdown () {
this.loader
.of(LoadingSpinner(`Shutting down...`))
.displayDuringAsync( async () => {
await this.embassyApi.shutdownServer({ })
})
.catch(console.error)
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Shutting down...',
cssClass: 'loader',
})
await loader.present()
try {
await this.embassyApi.shutdownServer({ })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private setButtons (): void {
@@ -93,7 +109,7 @@ export class ServerShowPage {
{
title: 'Privacy and Security',
icon: 'shield-checkmark-outline',
action: () => this.navCtrl.navigateForward(['privacy'], { relativeTo: this.route }),
action: () => this.navCtrl.navigateForward(['security'], { relativeTo: this.route }),
},
{
title: 'LAN',
@@ -105,11 +121,6 @@ export class ServerShowPage {
icon: 'wifi',
action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
},
{
title: 'Developer Options',
icon: 'terminal-outline',
action: () => this.navCtrl.navigateForward(['developer'], { relativeTo: this.route }),
},
],
'Insights': [
{
@@ -155,15 +166,6 @@ export class ServerShowPage {
}
}
const LoadingSpinner: (m?: string) => LoadingOptions = (m) => {
const toMergeIn = m ? { message: m } : { }
return {
spinner: 'lines',
cssClass: 'loader',
...toMergeIn,
} as LoadingOptions
}
interface ServerSettings {
[key: string]: {
title: string

View File

@@ -1,8 +1,7 @@
import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { WifiService } from '../wifi.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
@Component({
@@ -20,16 +19,19 @@ export class WifiAddPage {
private readonly navCtrl: NavController,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly wifiService: WifiService,
) { }
async save (): Promise<void> {
this.loader.of({
message: 'Saving...',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
cssClass: 'loader',
}).displayDuringAsync(async () => {
})
await loader.present()
try {
await this.embassyApi.addWifi({
ssid: this.ssid,
password: this.password,
@@ -38,18 +40,22 @@ export class WifiAddPage {
connect: false,
})
this.navCtrl.back()
}).catch(e => {
console.error(e)
this.errToast.present(e.message)
})
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async saveAndConnect (): Promise<void> {
this.loader.of({
message: 'Connecting. This could take while...',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Connecting. This could take while...',
cssClass: 'loader',
}).displayDuringAsync(async () => {
})
await loader.present()
try {
await this.embassyApi.addWifi({
ssid: this.ssid,
password: this.password,
@@ -58,13 +64,14 @@ export class WifiAddPage {
connect: true,
})
const success = this.wifiService.confirmWifi(this.ssid)
if (success) {
this.navCtrl.back()
}
}).catch (e => {
console.error(e)
this.errToast.present(e.message)
})
if (success) {
this.navCtrl.back()
}
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
asIsOrder (a: any, b: any) {

View File

@@ -17,7 +17,7 @@
<ion-item>
<ion-label class="ion-text-wrap">
<p style="padding-bottom: 6px;">About</p>
<h2>Embassy will automatically connect to available networks, allowing you to remove the Ethernet cable.</h2>
<h2>Embassy will automatically connect to saved WiFi networks when they are available, allowing you to remove the Ethernet cable.</h2>
</ion-label>
</ion-item>

View File

@@ -1,9 +1,8 @@
import { Component } from '@angular/core'
import { ActionSheetController } from '@ionic/angular'
import { ActionSheetController, LoadingController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
import { ActionSheetButton } from '@ionic/core'
import { WifiService } from './wifi.service'
import { LoaderService } from 'src/app/services/loader.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'
@@ -19,7 +18,7 @@ export class WifiListPage {
constructor (
private readonly embassyApi: ApiService,
private readonly loader: LoaderService,
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly actionCtrl: ActionSheetController,
private readonly wifiService: WifiService,
@@ -57,29 +56,37 @@ export class WifiListPage {
// Let's add country code here
async connect (ssid: string): Promise<void> {
this.loader.of({
message: 'Connecting. This could take while...',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Connecting. This could take while...',
cssClass: 'loader',
}).displayDuringAsync(async () => {
})
await loader.present()
try {
await this.embassyApi.connectWifi({ ssid })
this.wifiService.confirmWifi(ssid)
}).catch(e => {
console.error(e)
this.errToast.present(e.message)
})
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
async delete (ssid: string): Promise<void> {
this.loader.of({
message: 'Deleting...',
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Deleting...',
cssClass: 'loader',
}).displayDuringAsync(async () => {
await this.embassyApi.deleteWifi({ ssid })
}).catch(e => {
console.error(e)
this.errToast.present(e.message)
})
await loader.present()
try {
await this.embassyApi.deleteWifi({ ssid })
} catch (e) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
}

View File

@@ -24,6 +24,9 @@ export module RR {
// server
export type SetShareStatsReq = WithExpire<{ value: any }> // server.config.share-stats
export type SetShareStatsRes = WithRevision<null>
export type GetServerLogsReq = { before?: string } // server.logs
export type GetServerLogsRes = Log[]

View File

@@ -34,6 +34,11 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
// server
protected abstract setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes>
setShareStats = (params: RR.SetShareStatsReq) => this.syncResponse(
() => this.setShareStatsRaw(params),
)()
abstract getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
abstract getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes>

View File

@@ -43,6 +43,10 @@ export class LiveApiService extends ApiService {
// server
async setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes> {
return this.http.rpcRequest( { method: 'server.config.share-stats', params })
}
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
return this.http.rpcRequest( { method: 'server.logs', params })
}

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
import { pauseFor } from '../../../util/misc.util'
import { ApiService } from './embassy-api.service'
import { Operation, PatchOp } from 'patch-db-client'
import { DataModel, 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 { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { Mock } from '../api.fixures'
@@ -27,39 +27,14 @@ export class MockApiService extends ApiService {
// db
async getRevisions (since: number): Promise<RR.GetRevisionsRes> {
// await pauseFor(2000)
// return {
// ...Mock.DbDump,
// id: this.nextSequence(),
// }
return this.http.rpcRequest<RR.GetRevisionsRes>({ method: 'db.revisions', params: { since } })
}
async getDump (): Promise<RR.GetDumpRes> {
// await pauseFor(2000)
// return {
// ...Mock.DbDump,
// id: this.nextSequence(),
// }
return this.http.rpcRequest<RR.GetDumpRes>({ method: 'db.dump' })
}
async setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
// await pauseFor(2000)
// return {
// response: null,
// revision: {
// id: this.nextSequence(),
// patch: [
// {
// op: PatchOp.REPLACE,
// path: params.pointer,
// value: params.value,
// },
// ],
// expireId: null,
// },
// }
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.put.ui', params })
}
@@ -77,6 +52,18 @@ export class MockApiService extends ApiService {
// server
async setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: '/server-info/share-stats',
value: params.value,
},
]
return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
}
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
await pauseFor(2000)
return Mock.ServerLogs
@@ -94,20 +81,19 @@ export class MockApiService extends ApiService {
async updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
await pauseFor(2000)
const path = '/server-info/status'
const patch = [
{
op: PatchOp.REPLACE,
path,
path: '/server-info/status',
value: ServerStatus.Updating,
},
]
const res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
setTimeout(() => {
setTimeout(async () => {
const patch = [
{
op: PatchOp.REPLACE,
path,
path: '/server-info/status',
value: ServerStatus.Running,
},
{
@@ -116,7 +102,7 @@ export class MockApiService extends ApiService {
value: this.config.version + '.1',
},
]
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
// quickly revert patch to proper version to prevent infinite refresh loop
const patch2 = [
{

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { IonicSafeString, ToastController } from '@ionic/angular'
import { ToastButton } from '@ionic/core'
import { RequestError } from './http.service'
@Injectable({
providedIn: 'root',
@@ -12,11 +12,24 @@ export class ErrorToastService {
private readonly toastCtrl: ToastController,
) { }
async present (message: string | IonicSafeString, link?: string): Promise<void> {
async present (e: RequestError, link?: string): Promise<void> {
console.error(e)
if (this.toast) return
let message: string | IonicSafeString
if (e.status) message = String(e.status)
if (e.message) message = `${message ? message + ' ' : ''}${e.message}`
if (e.data) message = `${message ? message + '. ' : ''}${e.data.code}: ${e.data.message}`
if (!message) {
message = 'Unknown Error.'
link = 'https://docs.start9.com'
}
if (link) {
message = new IonicSafeString(message + `<br /><br /><a href=${link} target="_blank" style="color: white;">Get Help</a>`)
message = new IonicSafeString(`${message}<br /><br /><a href=${link} target="_blank" style="color: white;">Get Help</a>`)
}
this.toast = await this.toastCtrl.create({

View File

@@ -1,78 +0,0 @@
import { Injectable } from '@angular/core'
import { concatMap, finalize } from 'rxjs/operators'
import { Observable, from, Subject } from 'rxjs'
import { fromAsync$, fromAsyncP, emitAfter$, fromSync$ } from '../util/rxjs.util'
import { LoadingController } from '@ionic/angular'
import { LoadingOptions } from '@ionic/core'
@Injectable({
providedIn: 'root',
})
export class LoaderService {
private loadingOptions: LoadingOptions = defaultOptions()
constructor (private readonly loadingCtrl: LoadingController) { }
private loader: HTMLIonLoadingElement
public get ionLoader (): HTMLIonLoadingElement {
return this.loader
}
public get ctrl () {
return this.loadingCtrl
}
private setOptions (l: LoadingOptions): LoaderService {
this.loadingOptions = l
return this
}
of (overrideOptions: LoadingOptions): LoaderService {
return new LoaderService(this.loadingCtrl).setOptions(Object.assign(defaultOptions(), overrideOptions))
}
displayDuring$<T> (o: Observable<T>): Observable<T> {
let shouldDisplay = true
const displayIfItsBeenAtLeast = 10 // ms
return fromAsync$(
async () => {
this.loader = await this.loadingCtrl.create(this.loadingOptions)
emitAfter$(displayIfItsBeenAtLeast).subscribe(() => { if (shouldDisplay) this.loader.present() })
},
).pipe(
concatMap(() => o),
finalize(() => {
this.loader.dismiss(); shouldDisplay = false; this.loader = undefined
}),
)
}
displayDuringP<T> (p: Promise<T>): Promise<T> {
return this.displayDuring$(from(p)).toPromise()
}
displayDuringAsync<T> (thunk: () => Promise<T>): Promise<T> {
return this.displayDuringP(fromAsyncP(thunk))
}
}
export function markAsLoadingDuring$<T> ($trigger$: Subject<boolean>, o: Observable<T>): Observable<T> {
let shouldBeOn = true
const displayIfItsBeenAtLeast = 5 // ms
return fromSync$(() => {
emitAfter$(displayIfItsBeenAtLeast).subscribe(() => { if (shouldBeOn) $trigger$.next(true) })
}).pipe(
concatMap(() => o),
finalize(() => {
$trigger$.next(false)
shouldBeOn = false
}),
)
}
const defaultOptions: () => LoadingOptions = () => ({
spinner: 'lines',
cssClass: 'loader',
backdropDismiss: true,
})

View File

@@ -44,7 +44,6 @@ export class PatchDbService {
.pipe(debounceTime(500))
.subscribe({
next: cache => {
console.log('saving cacheee: ', JSON.parse(JSON.stringify(cache)))
this.connectionStatus$.next(ConnectionStatus.Connected)
this.bootstrapper.update(cache)
},
@@ -54,11 +53,11 @@ export class PatchDbService {
// this.start()
},
complete: () => {
console.error('patch-db-sync sub COMPLETE')
console.warn('patch-db-sync sub COMPLETE')
},
})
} catch (e) {
console.log('Failed to initialize PatchDB', e)
console.error('Failed to initialize PatchDB', e)
}
}
@@ -84,12 +83,12 @@ export class PatchDbService {
console.log('WATCHING', ...args)
return this.patchDb.store.watch$(...(args as []))
.pipe(
tap(data => console.log('CHANGE IN STORE', data, ...args)),
tap(data => console.log('NEW VALUE', data, ...args)),
catchError(e => {
console.error(e)
console.error('Error watching Patch DB', e)
return of(e.message)
}),
finalize(() => console.log('UNSUBSCRIBING')),
finalize(() => console.log('UNSUBSCRIBING', ...args)),
)
}
}

View File

@@ -3,7 +3,7 @@ import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.
import { ApiService } from './api/embassy/embassy-api.service'
import { ConfigSpec } from '../pkg-config/config-types'
import { ConfigCursor } from '../pkg-config/config-cursor'
import { SSHService } from '../pages/server-routes/developer-routes/dev-ssh-keys/ssh.service'
import { SSHService } from '../pages/server-routes/security-routes/ssh-keys/ssh.service'
import { TrackingModalController } from './tracking-modal-controller.service'
@Injectable({
@@ -34,8 +34,8 @@ export class ServerConfigService {
}
saveFns: { [key: string]: (val: any) => Promise<any> } = {
autoCheckUpdates: async (value: boolean) => {
return this.embassyApi.setDbValue({ pointer: 'ui/auto-check-updates', value })
autoCheckUpdates: async (enabled: boolean) => {
return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled })
},
ssh: async (pubkey: string) => {
return this.sshService.add(pubkey)
@@ -46,6 +46,9 @@ export class ServerConfigService {
// packageMarketplace: async (url: string) => {
// return this.embassyApi.setPackageMarketplace({ url })
// },
shareStats: async (enabled: boolean) => {
return this.embassyApi.setShareStats({ value: enabled })
},
// password: async (password: string) => {
// return this.embassyApi.updatePassword({ password })
// },
@@ -72,8 +75,9 @@ const serverConfig: ConfigSpec = {
},
eosMarketplace: {
type: 'boolean',
name: 'Use Tor',
description: `Use Start9's Tor Hidden Service Marketplace (instead of clearnet).`,
name: 'Tor Only Marketplace',
description: `Use Start9's Tor (instead of clearnet) Marketplace.`,
changeWarning: 'This will result in higher latency and slower download times.',
default: false,
},
// packageMarketplace: {
@@ -87,6 +91,12 @@ const serverConfig: ConfigSpec = {
// masked: false,
// copyable: false,
// },
shareStats: {
type: 'boolean',
name: 'Share Anonymous Statistics',
description: 'Start9 uses this information to identify bugs quickly and improve EmbassyOS. The information is 100% anonymous and transmitted over Tor.',
default: false,
},
// password: {
// type: 'string',
// name: 'Change Password',

View File

@@ -13,6 +13,7 @@ import { DataModel, PackageDataEntry } from './patch-db/data-model'
import { PatchDbService } from './patch-db/patch-db.service'
import { filter, take } from 'rxjs/operators'
import { isEmptyObject } from '../util/misc.util'
import { ApiService } from './api/embassy/embassy-api.service'
@Injectable({
providedIn: 'root',
@@ -28,6 +29,7 @@ export class StartupAlertsService {
private readonly modalCtrl: ModalController,
private readonly marketplaceService: MarketplaceService,
private readonly marketplaceApi: MarketplaceApiService,
private readonly embassyApi: ApiService,
private readonly emver: Emver,
private readonly wizardBaker: WizardBaker,
private readonly patch: PatchDbService,
@@ -120,18 +122,18 @@ export class StartupAlertsService {
private async displayOsWelcome (): Promise<boolean> {
return new Promise(async resolve => {
const modal = await this.modalCtrl.create({
backdropDismiss: false,
component: OSWelcomePage,
presentingElement: await this.modalCtrl.getTop(),
componentProps: {
version: this.config.version,
},
})
await modal.present()
modal.onWillDismiss().then(res => {
return resolve(res.data)
modal.onWillDismiss().then(() => {
this.embassyApi.setDbValue({ pointer: '/welcome-ack', value: this.config.version })
.catch()
return resolve(true)
})
await modal.present()
})
}

View File

@@ -73,6 +73,10 @@ ion-toast {
--border-color: var(--ion-color-warning);
}
.success-toast {
--border-color: var(--ion-color-success);
}
.error-toast {
--border-color: var(--ion-color-danger);
width: 40%;