From 2d4ecd309666650a98b5883333058d2936c6296f Mon Sep 17 00:00:00 2001 From: Drew Ansbacher Date: Mon, 31 Jan 2022 19:46:41 -0700 Subject: [PATCH] alt marketplace feature re-arrange use url api proxy function matt comments addressed delete cache on marketplace load failure --- .../marketplace-routes/marketplace.service.ts | 86 ++++-- .../marketplaces/marketplaces.module.ts | 24 ++ .../marketplaces/marketplaces.page.html | 52 ++++ .../marketplaces/marketplaces.page.scss | 7 + .../marketplaces/marketplaces.page.ts | 277 +++++++++++++++++ .../server-routes/server-routing.module.ts | 53 +++- .../server-show/server-show.page.ts | 103 +++++-- .../pages/server-routes/wifi/wifi.page.html | 128 ++++++-- .../app/pages/server-routes/wifi/wifi.page.ts | 109 ++++--- .../ui/src/app/services/api/api.types.ts | 153 ++++++---- .../app/services/api/embassy-api.service.ts | 279 ++++++++++++------ .../services/api/embassy-live-api.service.ts | 252 +++++++++++----- .../services/api/embassy-mock-api.service.ts | 120 ++++---- .../ui/src/app/services/api/mock-patch.ts | 9 + .../src/app/services/patch-db/data-model.ts | 11 + .../app/services/startup-alerts.service.ts | 163 +++++----- 16 files changed, 1311 insertions(+), 515 deletions(-) create mode 100644 frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.module.ts create mode 100644 frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html create mode 100644 frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.scss create mode 100644 frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts index b76ea3739..da37949f6 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace.service.ts @@ -1,5 +1,9 @@ import { Injectable } from '@angular/core' -import { MarketplaceData, MarketplaceEOS, MarketplacePkg } from 'src/app/services/api/api.types' +import { + MarketplaceData, + MarketplaceEOS, + MarketplacePkg, +} from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { Emver } from 'src/app/services/emver.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' @@ -12,38 +16,59 @@ export class MarketplaceService { data: MarketplaceData eos: MarketplaceEOS pkgs: MarketplacePkg[] = [] - releaseNotes: { [id: string]: { - [version: string]: string - } } = { } + releaseNotes: { + [id: string]: { + [version: string]: string + } + } = {} - constructor ( + constructor( private readonly api: ApiService, private readonly emver: Emver, private readonly patch: PatchDbService, - ) { } + ) {} - get eosUpdateAvailable () { - return this.emver.compare(this.eos.version, this.patch.data['server-info'].version) === 1 + get eosUpdateAvailable() { + return ( + this.emver.compare( + this.eos.version, + this.patch.data['server-info'].version, + ) === 1 + ) } - async load (): Promise { - const [data, eos, pkgs] = await Promise.all([ - this.api.getMarketplaceData({ }), - this.api.getEos({ - 'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'], - }), - this.getPkgs(1, 100), - ]) - this.data = data - this.eos = eos - this.pkgs = pkgs + async load(): Promise { + try { + const [data, eos, pkgs] = await Promise.all([ + this.api.getMarketplaceData({}), + this.api.getEos({ + 'eos-version-compat': + this.patch.getData()['server-info']['eos-version-compat'], + }), + this.getPkgs(1, 100), + ]) + this.data = data + this.eos = eos + this.pkgs = pkgs + } catch (e) { + this.data = undefined + this.eos = undefined + this.pkgs = [] + throw e + } } - async getUpdates (localPkgs: { [id: string]: PackageDataEntry }) : Promise { - const idAndCurrentVersions = Object.keys(localPkgs).map(key => ({ id: key, version: localPkgs[key].manifest.version })) + async getUpdates(localPkgs: { + [id: string]: PackageDataEntry + }): Promise { + const idAndCurrentVersions = Object.keys(localPkgs).map(key => ({ + id: key, + version: localPkgs[key].manifest.version, + })) const latestPkgs = await this.api.getMarketplacePkgs({ ids: idAndCurrentVersions, - 'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'], + 'eos-version-compat': + this.patch.getData()['server-info']['eos-version-compat'], }) return latestPkgs.filter(latestPkg => { @@ -53,10 +78,11 @@ export class MarketplaceService { }) } - async getPkg (id: string, version = '*'): Promise { + async getPkg(id: string, version = '*'): Promise { const pkgs = await this.api.getMarketplacePkgs({ ids: [{ id, version }], - 'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'], + 'eos-version-compat': + this.patch.getData()['server-info']['eos-version-compat'], }) const pkg = pkgs.find(pkg => pkg.manifest.id == id) @@ -67,19 +93,21 @@ export class MarketplaceService { } } - async getReleaseNotes (id: string): Promise { + async getReleaseNotes(id: string): Promise { this.releaseNotes[id] = await this.api.getReleaseNotes({ id }) } - private async getPkgs (page: number, perPage: number) : Promise { + private async getPkgs( + page: number, + perPage: number, + ): Promise { const pkgs = await this.api.getMarketplacePkgs({ page: String(page), 'per-page': String(perPage), - 'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'], + 'eos-version-compat': + this.patch.getData()['server-info']['eos-version-compat'], }) return pkgs } } - - diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.module.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.module.ts new file mode 100644 index 000000000..e46aa7324 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { MarketplacesPage } from './marketplaces.page' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: MarketplacesPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + SharingModule, + ], + declarations: [MarketplacesPage], +}) +export class MarketplacesPageModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html new file mode 100644 index 000000000..d244d58c5 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.html @@ -0,0 +1,52 @@ + + + + + + Marketplace Settings + + + + + + Saved Marketplaces + + + + + Add alternative marketplace + + + + + +
+ + +

{{ mp.value.name }}

+

{{ mp.value.url }}

+
+ + Selected + +
+
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.scss b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.scss new file mode 100644 index 000000000..8be298f07 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.scss @@ -0,0 +1,7 @@ +.skeleton-parts { + ion-button::part(native) { + padding-inline-start: 0; + padding-inline-end: 0; + }; + padding-bottom: 6px; +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts new file mode 100644 index 000000000..b55e249f9 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/marketplaces/marketplaces.page.ts @@ -0,0 +1,277 @@ +import { Component } from '@angular/core' +import { + ActionSheetController, + LoadingController, + ModalController, +} from '@ionic/angular' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ActionSheetButton } from '@ionic/core' +import { ErrorToastService } from 'src/app/services/error-toast.service' +import { ValueSpecObject } from 'src/app/pkg-config/config-types' +import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' +import { PatchDbService } from '../../../services/patch-db/patch-db.service' +import { v4 } from 'uuid' +import { MarketplaceService } from '../../marketplace-routes/marketplace.service' +import { + DataModel, + UIData, + UIMarketplaceData, +} from '../../../services/patch-db/data-model' + +@Component({ + selector: 'marketplaces', + templateUrl: 'marketplaces.page.html', + styleUrls: ['marketplaces.page.scss'], +}) +export class MarketplacesPage { + constructor( + private readonly api: ApiService, + private readonly loadingCtrl: LoadingController, + private readonly modalCtrl: ModalController, + private readonly errToast: ErrorToastService, + private readonly actionCtrl: ActionSheetController, + private readonly marketplaceService: MarketplaceService, + public readonly patch: PatchDbService, + ) {} + + async presentModalAdd() { + const marketplaceSpec = getMarketplaceValueSpec() + const modal = await this.modalCtrl.create({ + component: GenericFormPage, + componentProps: { + title: marketplaceSpec.name, + spec: marketplaceSpec.spec, + buttons: [ + { + text: 'Save for Later', + handler: (value: { url: string }) => { + this.save(value.url) + }, + }, + { + text: 'Save and Connect', + handler: (value: { url: string }) => { + this.saveAndConnect(value.url) + }, + isSubmit: true, + }, + ], + }, + cssClass: 'alertlike-modal', + }) + + await modal.present() + } + + async presentAction(id: string) { + // no need to view actions if is selected marketplace + if (id === this.patch.data.ui.marketplace['selected-id']) return + + const buttons: ActionSheetButton[] = [ + { + text: 'Forget', + icon: 'trash', + role: 'destructive', + handler: () => { + this.delete(id) + }, + }, + { + text: 'Connect to marketplace', + handler: () => { + this.connect(id) + }, + }, + ] + + const action = await this.actionCtrl.create({ + header: id, + subHeader: 'Manage marketplaces', + mode: 'ios', + buttons, + }) + + await action.present() + } + + private async connect(id: string): Promise { + const marketplace = JSON.parse( + JSON.stringify(this.patch.data.ui.marketplace), + ) + const newMarketplace = marketplace.options[id] + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Validating Marketplace...', + cssClass: 'loader', + }) + await loader.present() + + try { + await this.api.getMarketplaceData({}, newMarketplace.url) + } catch (e) { + this.errToast.present({ + message: `Could not connect to ${newMarketplace.url}`, + } as any) + loader.dismiss() + return + } + + loader.message = 'Changing Marketplace...' + + try { + marketplace['selected-id'] = id + await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) + } catch (e) { + this.errToast.present(e) + loader.dismiss() + } + + loader.message = 'Syncing store...' + + try { + await this.marketplaceService.load() + } catch (e) { + this.errToast.present({ + message: `Error syncing marketplace data`, + } as any) + } finally { + loader.dismiss() + } + } + + private async delete(id: string): Promise { + const marketplace = JSON.parse( + JSON.stringify(this.patch.data.ui.marketplace), + ) + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Deleting...', + cssClass: 'loader', + }) + await loader.present() + + try { + delete marketplace.options[id] + await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) + } catch (e) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + + private async save(url: string): Promise { + const marketplace = JSON.parse( + JSON.stringify(this.patch.data.ui.marketplace), + ) as UIMarketplaceData + + // no-op on duplicates + const currentUrls = Object.values(marketplace.options).map( + u => new URL(u.url).hostname, + ) + if (currentUrls.includes(new URL(url).hostname)) return + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Validating Marketplace...', + cssClass: 'loader', + }) + + await loader.present() + + try { + const id = v4() + const { name } = await this.api.getMarketplaceData({}, url) + marketplace.options[id] = { name, url } + } catch (e) { + this.errToast.present({ message: `Could not connect to ${url}` } as any) + loader.dismiss() + return + } + + loader.message = 'Saving...' + + try { + await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) + } catch (e) { + this.errToast.present({ message: `Error saving marketplace data` } as any) + } finally { + loader.dismiss() + } + } + + private async saveAndConnect(url: string): Promise { + const marketplace = JSON.parse( + JSON.stringify(this.patch.data.ui.marketplace), + ) as UIMarketplaceData + + // no-op on duplicates + const currentUrls = Object.values(marketplace.options).map( + u => new URL(u.url).hostname, + ) + if (currentUrls.includes(new URL(url).hostname)) return + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Validating Marketplace...', + cssClass: 'loader', + }) + await loader.present() + + try { + const id = v4() + const { name } = await this.api.getMarketplaceData({}, url) + marketplace.options[id] = { name, url } + marketplace['selected-id'] = id + } catch (e) { + this.errToast.present({ message: `Could not connect to ${url}` } as any) + loader.dismiss() + return + } + + loader.message = 'Saving...' + + try { + await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace }) + } catch (e) { + this.errToast.present({ message: `Error saving marketplace data` } as any) + loader.dismiss() + return + } + + loader.message = 'Syncing store...' + + try { + await this.marketplaceService.load() + } catch (e) { + this.errToast.present({ + message: `Error syncing marketplace data`, + } as any) + } finally { + loader.dismiss() + } + } +} + +function getMarketplaceValueSpec(): ValueSpecObject { + return { + type: 'object', + name: 'Add Marketplace', + 'unique-by': null, + spec: { + url: { + type: 'string', + name: 'URL', + description: 'The fully-qualified URL of the alternative marketplace.', + nullable: false, + masked: false, + copyable: false, + pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}`, + 'pattern-description': 'Must be a valid URL', + placeholder: 'e.g. https://example.org', + }, + }, + } +} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts index 3e0f51762..d8519bdbd 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -4,11 +4,17 @@ import { Routes, RouterModule } from '@angular/router' const routes: Routes = [ { path: '', - loadChildren: () => import('./server-show/server-show.module').then(m => m.ServerShowPageModule), + loadChildren: () => + import('./server-show/server-show.module').then( + m => m.ServerShowPageModule, + ), }, { path: 'backup', - loadChildren: () => import('./server-backup/server-backup.module').then(m => m.ServerBackupPageModule), + loadChildren: () => + import('./server-backup/server-backup.module').then( + m => m.ServerBackupPageModule, + ), }, { path: 'lan', @@ -16,35 +22,60 @@ const routes: Routes = [ }, { path: 'logs', - loadChildren: () => import('./server-logs/server-logs.module').then(m => m.ServerLogsPageModule), + loadChildren: () => + import('./server-logs/server-logs.module').then( + m => m.ServerLogsPageModule, + ), + }, + { + path: 'marketplaces', + loadChildren: () => + import('./marketplaces/marketplaces.module').then( + m => m.MarketplacesPageModule, + ), }, { path: 'metrics', - loadChildren: () => import('./server-metrics/server-metrics.module').then(m => m.ServerMetricsPageModule), + loadChildren: () => + import('./server-metrics/server-metrics.module').then( + m => m.ServerMetricsPageModule, + ), }, { path: 'preferences', - loadChildren: () => import('./preferences/preferences.module').then( m => m.PreferencesPageModule), + loadChildren: () => + import('./preferences/preferences.module').then( + m => m.PreferencesPageModule, + ), }, { path: 'restore', - loadChildren: () => import('./restore/restore.component.module').then( m => m.RestorePageModule), + loadChildren: () => + import('./restore/restore.component.module').then( + m => m.RestorePageModule, + ), }, { path: 'sessions', - loadChildren: () => import('./sessions/sessions.module').then( m => m.SessionsPageModule), + loadChildren: () => + import('./sessions/sessions.module').then(m => m.SessionsPageModule), }, { path: 'specs', - loadChildren: () => import('./server-specs/server-specs.module').then(m => m.ServerSpecsPageModule), + loadChildren: () => + import('./server-specs/server-specs.module').then( + m => m.ServerSpecsPageModule, + ), }, { path: 'ssh', - loadChildren: () => import('./ssh-keys/ssh-keys.module').then( m => m.SSHKeysPageModule), + loadChildren: () => + import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule), }, { path: 'wifi', - loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiPageModule), + loadChildren: () => + import('./wifi/wifi.module').then(m => m.WifiPageModule), }, ] @@ -52,4 +83,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class ServerRoutingModule { } \ No newline at end of file +export class ServerRoutingModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index e1d813ee4..0249bd57f 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -1,5 +1,10 @@ import { Component } from '@angular/core' -import { AlertController, LoadingController, NavController, IonicSafeString } from '@ionic/angular' +import { + AlertController, + LoadingController, + NavController, + IonicSafeString, +} from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ActivatedRoute } from '@angular/router' import { ErrorToastService } from 'src/app/services/error-toast.service' @@ -16,7 +21,7 @@ import { map } from 'rxjs/operators' export class ServerShowPage { ServerStatus = ServerStatus - constructor ( + constructor( private readonly alertCtrl: AlertController, private readonly loadingCtrl: LoadingController, private readonly errToast: ErrorToastService, @@ -24,12 +29,13 @@ export class ServerShowPage { private readonly navCtrl: NavController, private readonly route: ActivatedRoute, public readonly patch: PatchDbService, - ) { } + ) {} - async presentAlertRestart () { + async presentAlertRestart() { const alert = await this.alertCtrl.create({ header: 'Confirm', - message: 'Are you sure you want to restart your Embassy? It can take several minutes to come back online.', + message: + 'Are you sure you want to restart your Embassy? It can take several minutes to come back online.', buttons: [ { text: 'Cancel', @@ -47,11 +53,12 @@ export class ServerShowPage { await alert.present() } - async presentAlertShutdown () { + async presentAlertShutdown() { const sts = this.patch.data['server-info'].status const alert = await this.alertCtrl.create({ header: 'Warning', - message: 'Are you sure you want to power down your Embassy? This can take several minutes, and your Embassy will not come back online automatically. To power on again, You will need to physically unplug your Embassy and plug it back in.', + message: + 'Are you sure you want to power down your Embassy? This can take several minutes, and your Embassy will not come back online automatically. To power on again, You will need to physically unplug your Embassy and plug it back in.', buttons: [ { text: 'Cancel', @@ -69,11 +76,13 @@ export class ServerShowPage { await alert.present() } - async presentAlertSystemRebuild () { + async presentAlertSystemRebuild() { const minutes = Object.keys(this.patch.data['package-data']).length * 2 const alert = await this.alertCtrl.create({ header: 'System Rebuild', - message: new IonicSafeString(`Important: This will tear down all service containers and rebuild them from scratch. This may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`), + message: new IonicSafeString( + `Important: This will tear down all service containers and rebuild them from scratch. This may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your Embassy.`, + ), buttons: [ { text: 'Cancel', @@ -91,7 +100,7 @@ export class ServerShowPage { await alert.present() } - private async restart () { + private async restart() { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Restarting...', @@ -100,7 +109,7 @@ export class ServerShowPage { await loader.present() try { - await this.embassyApi.restartServer({ }) + await this.embassyApi.restartServer({}) } catch (e) { this.errToast.present(e) } finally { @@ -108,7 +117,7 @@ export class ServerShowPage { } } - private async shutdown () { + private async shutdown() { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Shutting down...', @@ -117,7 +126,7 @@ export class ServerShowPage { await loader.present() try { - await this.embassyApi.shutdownServer({ }) + await this.embassyApi.shutdownServer({}) } catch (e) { this.errToast.present(e) } finally { @@ -125,7 +134,7 @@ export class ServerShowPage { } } - private async systemRebuild () { + private async systemRebuild() { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Hard Restarting...', @@ -134,7 +143,7 @@ export class ServerShowPage { await loader.present() try { - await this.embassyApi.systemRebuild({ }) + await this.embassyApi.systemRebuild({}) } catch (e) { this.errToast.present(e) } finally { @@ -143,12 +152,13 @@ export class ServerShowPage { } settings: ServerSettings = { - 'Backups': [ + Backups: [ { title: 'Create Backup', description: 'Back up your Embassy and all its services', icon: 'save-outline', - action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }), detail: true, disabled: of(false), }, @@ -156,17 +166,25 @@ export class ServerShowPage { title: 'Restore From Backup', description: 'Restore one or more services from a prior backup', icon: 'color-wand-outline', - action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }), detail: true, - disabled: this.patch.watch$('server-info', 'status').pipe(map(status => [ServerStatus.Updated, ServerStatus.BackingUp].includes(status))), + disabled: this.patch + .watch$('server-info', 'status') + .pipe( + map(status => + [ServerStatus.Updated, ServerStatus.BackingUp].includes(status), + ), + ), }, ], - 'Insights': [ + Insights: [ { title: 'About', description: 'Basic information about your Embassy', icon: 'information-circle-outline', - action: () => this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }), detail: true, disabled: of(false), }, @@ -174,7 +192,8 @@ export class ServerShowPage { title: 'Monitor', description: 'CPU, disk, memory, and other useful metrics', icon: 'pulse', - action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), detail: true, disabled: of(false), }, @@ -182,17 +201,21 @@ export class ServerShowPage { title: 'Logs', description: 'Raw, unfiltered device logs', icon: 'newspaper-outline', - action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }), detail: true, disabled: of(false), }, ], - 'Settings': [ + Settings: [ { title: 'Preferences', description: 'Device name, background tasks', icon: 'options-outline', - action: () => this.navCtrl.navigateForward(['preferences'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['preferences'], { + relativeTo: this.route, + }), detail: true, disabled: of(false), }, @@ -200,7 +223,8 @@ export class ServerShowPage { title: 'LAN', description: 'Access your Embassy on the Local Area Network', icon: 'home-outline', - action: () => this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }), detail: true, disabled: of(false), }, @@ -208,16 +232,28 @@ export class ServerShowPage { title: 'SSH', description: 'Access your Embassy from the command line', icon: 'terminal-outline', - action: () => this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }), detail: true, disabled: of(false), - }, { title: 'WiFi', description: 'Add or remove WiFi networks', icon: 'wifi', - action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + detail: true, + disabled: of(false), + }, + { + title: 'Marketplace Settings', + description: 'Add or remove marketplaces', + icon: 'storefront', + action: () => + this.navCtrl.navigateForward(['marketplaces'], { + relativeTo: this.route, + }), detail: true, disabled: of(false), }, @@ -225,12 +261,15 @@ export class ServerShowPage { title: 'Active Sessions', description: 'View and manage device access', icon: 'desktop-outline', - action: () => this.navCtrl.navigateForward(['sessions'], { relativeTo: this.route }), + action: () => + this.navCtrl.navigateForward(['sessions'], { + relativeTo: this.route, + }), detail: true, disabled: of(false), }, ], - 'Power': [ + Power: [ { title: 'Restart', description: '', @@ -258,7 +297,7 @@ export class ServerShowPage { ], } - asIsOrder () { + asIsOrder() { return 0 } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html index 0d2314a2f..320c36928 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -15,13 +15,19 @@ -

- Adding WiFi credentials to your Embassy allows you to remove the Ethernet cable and move the device anywhere you want. Embassy will automatically connect to available networks. - View instructions + Adding WiFi credentials to your Embassy allows you to remove the + Ethernet cable and move the device anywhere you want. Embassy will + automatically connect to available networks. + View instructions

@@ -29,9 +35,16 @@ Country - + - {{ wifi.country }} - {{ this.countries[wifi.country] }} + {{ wifi.country }} - {{ this.countries[wifi.country] }} Select Country @@ -40,20 +53,26 @@ Saved Networks - + - + Available Networks - + - + @@ -61,29 +80,88 @@ Saved Networks - -
- + +
+ {{ ssid.key }} - - - - + + + +
- + Available Networks - - + {{ avWifi.ssid }} - - - - + + + + - Other + Join other network
-
\ No newline at end of file + diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index e563346f0..14f8eb8cb 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -1,5 +1,11 @@ import { Component } from '@angular/core' -import { ActionSheetController, AlertController, LoadingController, ModalController, ToastController } 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 { ActionSheetButton } from '@ionic/core' @@ -17,10 +23,12 @@ import { ConfigService } from 'src/app/services/config.service' }) export class WifiPage { loading = true - wifi: RR.GetWifiRes = { } as any - countries = require('../../../util/countries.json') as { [key: string]: string } + wifi: RR.GetWifiRes = {} as any + countries = require('../../../util/countries.json') as { + [key: string]: string + } - constructor ( + constructor( private readonly api: ApiService, private readonly toastCtrl: ToastController, private readonly alertCtrl: AlertController, @@ -29,9 +37,9 @@ export class WifiPage { private readonly errToast: ErrorToastService, private readonly actionCtrl: ActionSheetController, private readonly config: ConfigService, - ) { } + ) {} - async ngOnInit () { + async ngOnInit() { try { await this.getWifi() } catch (e) { @@ -41,24 +49,24 @@ export class WifiPage { } } - async getWifi (timeout?: number): Promise { - this.wifi = await this.api.getWifi({ }, timeout) + async getWifi(timeout?: number): Promise { + this.wifi = await this.api.getWifi({}, timeout) if (!this.wifi.country) { await this.presentAlertCountry() } } - async presentAlertCountry (): Promise { + async presentAlertCountry(): Promise { if (!this.config.isLan) { const alert = await this.alertCtrl.create({ header: 'Cannot Complete Action', - message: 'You must be connected to your Emassy via LAN to change the country.', + message: + 'You must be connected to your Emassy via LAN to change the country.', buttons: [ { text: 'OK', role: 'cancel', }, - ], cssClass: 'wide-alert enter-click', }) @@ -66,19 +74,22 @@ export class WifiPage { return } - 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 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', - message: 'Warning: Changing the country will delete all saved networks from the Embassy.', + subHeader: + 'Warning: Changing the country will delete all saved networks from the Embassy.', inputs, buttons: [ { @@ -92,12 +103,12 @@ export class WifiPage { }, }, ], - cssClass: 'wide-alert enter-click', + cssClass: 'wide-alert enter-click select-warning', }) await alert.present() } - async presentModalAdd (ssid?: string, needsPW: boolean = true) { + async presentModalAdd(ssid?: string, needsPW: boolean = true) { const wifiSpec = getWifiValueSpec(ssid, needsPW) const modal = await this.modalCtrl.create({ component: GenericFormPage, @@ -107,13 +118,13 @@ export class WifiPage { buttons: [ { text: 'Save for Later', - handler: async (value: { ssid: string, password: string }) => { + 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 }) => { + handler: async (value: { ssid: string; password: string }) => { await this.saveAndConnect(value.ssid, value.password) }, isSubmit: true, @@ -125,7 +136,7 @@ export class WifiPage { await modal.present() } - async presentAction (ssid: string) { + async presentAction(ssid: string) { const buttons: ActionSheetButton[] = [ { text: 'Forget', @@ -138,15 +149,13 @@ export class WifiPage { ] if (ssid !== this.wifi.connected) { - buttons.unshift( - { - text: 'Connect', - icon: 'wifi', - handler: () => { - this.connect(ssid) - }, + buttons.unshift({ + text: 'Connect', + icon: 'wifi', + handler: () => { + this.connect(ssid) }, - ) + }) } const action = await this.actionCtrl.create({ @@ -159,7 +168,7 @@ export class WifiPage { await action.present() } - private async setCountry (country: string): Promise { + private async setCountry(country: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', cssClass: 'loader', @@ -177,7 +186,10 @@ export class WifiPage { } } - private async confirmWifi (ssid: string, deleteOnFailure = false): Promise { + private async confirmWifi( + ssid: string, + deleteOnFailure = false, + ): Promise { const timeout = 4000 const maxAttempts = 5 let attempts = 0 @@ -210,10 +222,11 @@ export class WifiPage { } } - private async presentAlertSuccess (ssid: string): Promise { + private async presentAlertSuccess(ssid: string): Promise { 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.', + message: + 'Note. It may take several minutes to an hour for your Embassy to reconnect over Tor.', buttons: [ { text: 'Ok', @@ -226,7 +239,7 @@ export class WifiPage { await alert.present() } - private async presentToastFail (): Promise { + private async presentToastFail(): Promise { const toast = await this.toastCtrl.create({ header: 'Failed to connect:', message: `Check credentials and try again`, @@ -247,7 +260,7 @@ export class WifiPage { await toast.present() } - private async connect (ssid: string): Promise { + private async connect(ssid: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Connecting. This could take a while...', @@ -265,7 +278,7 @@ export class WifiPage { } } - private async delete (ssid: string): Promise { + private async delete(ssid: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Deleting...', @@ -284,7 +297,7 @@ export class WifiPage { } } - private async save (ssid: string, password: string): Promise { + private async save(ssid: string, password: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Saving...', @@ -307,7 +320,7 @@ export class WifiPage { } } - private async saveAndConnect (ssid: string, password: string): Promise { + private async saveAndConnect(ssid: string, password: string): Promise { const loader = await this.loadingCtrl.create({ spinner: 'lines', message: 'Connecting. This could take a while...', @@ -324,7 +337,6 @@ export class WifiPage { }) await this.confirmWifi(ssid, true) - } catch (e) { this.errToast.present(e) } finally { @@ -333,11 +345,15 @@ export class WifiPage { } } -function getWifiValueSpec (ssid?: string, needsPW: boolean = true): ValueSpecObject { +function getWifiValueSpec( + ssid?: string, + needsPW: boolean = true, +): ValueSpecObject { return { type: 'object', name: 'WiFi Credentials', - description: 'Enter the network SSID and password. You can connect now or save the network for later.', + description: + 'Enter the network SSID and password. You can connect now or save the network for later.', 'unique-by': null, spec: { ssid: { @@ -358,4 +374,3 @@ function getWifiValueSpec (ssid?: string, needsPW: boolean = true): ValueSpecObj }, } } - diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index 3f092d9f3..e1f94160c 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -1,25 +1,29 @@ import { Dump, Revision } from 'patch-db-client' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { DataModel, DependencyError, Manifest, URL } from 'src/app/services/patch-db/data-model' +import { + DataModel, + DependencyError, + Manifest, + URL, +} from 'src/app/services/patch-db/data-model' export module RR { - // DB export type GetRevisionsRes = Revision[] | Dump export type GetDumpRes = Dump - export type SetDBValueReq = WithExpire<{ pointer: string, value: any }> // db.put.ui + export type SetDBValueReq = WithExpire<{ pointer: string; value: any }> // db.put.ui export type SetDBValueRes = WithRevision // auth - 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 LogoutReq = { } // auth.logout + export type LogoutReq = {} // auth.logout export type LogoutRes = null // server @@ -27,27 +31,31 @@ export module RR { export type SetShareStatsReq = WithExpire<{ value: boolean }> // server.config.share-stats export type SetShareStatsRes = WithRevision - export type GetServerLogsReq = { cursor?: string, before_flag?: boolean, limit?: number } + export type GetServerLogsReq = { + cursor?: string + before_flag?: boolean + limit?: number + } export type GetServerLogsRes = LogsRes - export type GetServerMetricsReq = { } // server.metrics + export type GetServerMetricsReq = {} // server.metrics export type GetServerMetricsRes = Metrics - export type UpdateServerReq = WithExpire<{ }> // server.update + export type UpdateServerReq = WithExpire<{}> // server.update export type UpdateServerRes = WithRevision<'updating' | 'no-updates'> - export type RestartServerReq = { } // server.restart + export type RestartServerReq = {} // server.restart export type RestartServerRes = null - export type ShutdownServerReq = { } // server.shutdown + export type ShutdownServerReq = {} // server.shutdown export type ShutdownServerRes = null - export type SystemRebuildReq = { } // server.rebuild + export type SystemRebuildReq = {} // server.rebuild export type SystemRebuildRes = null // sessions - export type GetSessionsReq = { } // sessions.list + export type GetSessionsReq = {} // sessions.list export type GetSessionsRes = { current: string sessions: { [hash: string]: Session } @@ -71,7 +79,10 @@ export module RR { // notification - export type GetNotificationsReq = WithExpire<{ before?: number, limit?: number }> // notification.list + export type GetNotificationsReq = WithExpire<{ + before?: number + limit?: number + }> // notification.list export type GetNotificationsRes = WithRevision[]> export type DeleteNotificationReq = { id: number } // notification.delete @@ -85,18 +96,19 @@ export module RR { export type SetWifiCountryReq = { country: string } export type SetWifiCountryRes = null - export type GetWifiReq = { } + export type GetWifiReq = {} export type GetWifiRes = { ssids: { - [ssid: string]: number - }, - connected?: string, - country: string, - ethernet: boolean, + [ssid: string]: number + } + connected?: string + country: string + ethernet: boolean 'available-wifi': AvailableWifi[] -} + } - export type AddWifiReq = { // wifi.add + export type AddWifiReq = { + // wifi.add ssid: string password: string priority: number @@ -112,7 +124,7 @@ export module RR { // ssh - export type GetSSHKeysReq = { } // ssh.list + export type GetSSHKeysReq = {} // ssh.list export type GetSSHKeysRes = SSHKey[] export type AddSSHKeyReq = { key: string } // ssh.add @@ -123,10 +135,11 @@ export module RR { // backup - export type GetBackupTargetsReq = { } // backup.target.list + export type GetBackupTargetsReq = {} // backup.target.list export type GetBackupTargetsRes = { [id: string]: BackupTarget } - export type AddBackupTargetReq = { // backup.target.cifs.add + export type AddBackupTargetReq = { + // backup.target.cifs.add hostname: string path: string username: string @@ -140,10 +153,11 @@ export module RR { export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove export type RemoveBackupTargetRes = null - export type GetBackupInfoReq = { 'target-id': string, password: string } // backup.target.info + export type GetBackupInfoReq = { 'target-id': string; password: string } // backup.target.info export type GetBackupInfoRes = BackupInfo - export type CreateBackupReq = WithExpire<{ // backup.create + export type CreateBackupReq = WithExpire<{ + // backup.create 'target-id': string 'old-password': string | null password: string @@ -153,40 +167,58 @@ export module RR { // package export type GetPackagePropertiesReq = { id: string } // package.properties - export type GetPackagePropertiesRes = PackagePropertiesVersioned + export type GetPackagePropertiesRes = + PackagePropertiesVersioned - export type LogsRes = { entries: Log[], 'start-cursor'?: string, 'end-cursor'?: string } + export type LogsRes = { + entries: Log[] + 'start-cursor'?: string + 'end-cursor'?: string + } - export type GetPackageLogsReq = { id: string, cursor?: string, before_flag?: boolean, limit?: number } // package.logs + export type GetPackageLogsReq = { + id: string + cursor?: string + before_flag?: boolean + limit?: number + } // package.logs export type GetPackageLogsRes = LogsRes export type GetPackageMetricsReq = { id: string } // package.metrics export type GetPackageMetricsRes = Metric - export type InstallPackageReq = WithExpire<{ id: string, 'version-spec'?: string }> // package.install + export type InstallPackageReq = WithExpire<{ + id: string + 'version-spec'?: string + }> // package.install export type InstallPackageRes = WithRevision - export type DryUpdatePackageReq = { id: string, version: string } // package.update.dry + export type DryUpdatePackageReq = { id: string; version: string } // package.update.dry export type DryUpdatePackageRes = Breakages export type GetPackageConfigReq = { id: string } // package.config.get - export type GetPackageConfigRes = { spec: ConfigSpec, config: object } + export type GetPackageConfigRes = { spec: ConfigSpec; config: object } - export type DrySetPackageConfigReq = { id: string, config: object } // package.config.set.dry + export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry export type DrySetPackageConfigRes = Breakages export type SetPackageConfigReq = WithExpire // package.config.set export type SetPackageConfigRes = WithRevision - export type RestorePackagesReq = WithExpire<{ // package.backup.restore + export type RestorePackagesReq = WithExpire<{ + // package.backup.restore ids: string[] 'target-id': string - 'old-password': string | null, + 'old-password': string | null password: string }> export type RestorePackagesRes = WithRevision - export type ExecutePackageActionReq = { id: string, 'action-id': string, input?: object } // package.action + export type ExecutePackageActionReq = { + id: string + 'action-id': string + input?: object + } // package.action export type ExecutePackageActionRes = ActionResponse export type StartPackageReq = WithExpire<{ id: string }> // package.start @@ -207,7 +239,10 @@ export module RR { export type DeleteRecoveredPackageReq = { id: string } // package.delete-recovered export type DeleteRecoveredPackageRes = WithRevision - export type DryConfigureDependencyReq = { 'dependency-id': string, 'dependent-id': string } // package.dependency.configure.dry + export type DryConfigureDependencyReq = { + 'dependency-id': string + 'dependent-id': string + } // package.dependency.configure.dry export type DryConfigureDependencyRes = { 'old-config': object 'new-config': object @@ -216,7 +251,7 @@ export module RR { // marketplace - export type GetMarketplaceDataReq = { } + export type GetMarketplaceDataReq = { url?: string } export type GetMarketplaceDataRes = MarketplaceData export type GetMarketplaceEOSReq = { @@ -225,13 +260,14 @@ export module RR { export type GetMarketplaceEOSRes = MarketplaceEOS export type GetMarketplacePackagesReq = { - ids?: { id: string, version: string }[] + ids?: { id: string; version: string }[] 'eos-version-compat': string // iff !ids category?: string query?: string page?: string 'per-page'?: string + url?: string } export type GetMarketplacePackagesRes = MarketplacePkg[] @@ -240,14 +276,14 @@ export module RR { export type GetLatestVersionReq = { ids: string[] } export type GetLatestVersionRes = { [id: string]: string } - } export type WithExpire = { 'expire-id'?: string } & T -export type WithRevision = { response: T, revision?: Revision } +export type WithRevision = { response: T; revision?: Revision } export interface MarketplaceData { categories: string[] + name: string } export interface MarketplaceEOS { @@ -268,7 +304,7 @@ export interface MarketplacePkg { title: string icon: URL } - }, + } } export interface Breakages { @@ -318,7 +354,22 @@ export interface SessionMetadata { platforms: PlatformType[] } -export type PlatformType = 'cli' | 'ios' | 'ipad' | 'iphone' | 'android' | 'phablet' | 'tablet' | 'cordova' | 'capacitor' | 'electron' | 'pwa' | 'mobile' | 'mobileweb' | 'desktop' | 'hybrid' +export type PlatformType = + | 'cli' + | 'ios' + | 'ipad' + | 'iphone' + | 'android' + | 'phablet' + | 'tablet' + | 'cordova' + | 'capacitor' + | 'electron' + | 'pwa' + | 'mobile' + | 'mobileweb' + | 'desktop' + | 'hybrid' export type BackupTarget = DiskBackupTarget | CifsBackupTarget @@ -387,8 +438,8 @@ export interface EmbassyOsDiskInfo { } export interface BackupInfo { - version: string, - timestamp: string, + version: string + timestamp: string 'package-backups': { [id: string]: PackageBackupInfo } @@ -432,9 +483,11 @@ export enum NotificationLevel { Error = 'error', } -export type NotificationData = T extends 0 ? null : - T extends 1 ? BackupReport : - any +export type NotificationData = T extends 0 + ? null + : T extends 1 + ? BackupReport + : any export interface BackupReport { server: { @@ -451,5 +504,5 @@ export interface BackupReport { export interface AvailableWifi { ssid: string strength: number - security: string [] -} \ No newline at end of file + security: string[] +} diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index 20fcf3af2..4cebd8685 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -1,5 +1,13 @@ import { Subject, Observable } from 'rxjs' -import { Http, Update, Operation, Revision, Source, Store, RPCResponse } from 'patch-db-client' +import { + Http, + Update, + Operation, + Revision, + Source, + Store, + RPCResponse, +} from 'patch-db-client' import { RR } from './api.types' import { DataModel } from 'src/app/services/patch-db/data-model' import { RequestError } from '../http.service' @@ -10,53 +18,63 @@ export abstract class ApiService implements Source, Http { /** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */ // sequenceStream '_' is not used by the live api, but is overridden by the mock - watch$ (_?: Store): Observable>> { - return this.sync$.asObservable().pipe(map( result => ({ result, - jsonrpc: '2.0'}))) + watch$(_?: Store): Observable>> { + return this.sync$ + .asObservable() + .pipe(map(result => ({ result, jsonrpc: '2.0' }))) } // for getting static files: ex icons, instructions, licenses - abstract getStatic (url: string): Promise + abstract getStatic(url: string): Promise // db - abstract getRevisions (since: number): Promise + abstract getRevisions(since: number): Promise - abstract getDump (): Promise + abstract getDump(): Promise - protected abstract setDbValueRaw (params: RR.SetDBValueReq): Promise - setDbValue = (params: RR.SetDBValueReq) => this.syncResponse( - () => this.setDbValueRaw(params), - )() + protected abstract setDbValueRaw( + params: RR.SetDBValueReq, + ): Promise + setDbValue = (params: RR.SetDBValueReq) => + this.syncResponse(() => this.setDbValueRaw(params))() // auth - abstract login (params: RR.LoginReq): Promise + abstract login(params: RR.LoginReq): Promise - abstract logout (params: RR.LogoutReq): Promise + abstract logout(params: RR.LogoutReq): Promise - abstract getSessions (params: RR.GetSessionsReq): Promise + abstract getSessions(params: RR.GetSessionsReq): Promise - abstract killSessions (params: RR.KillSessionsReq): Promise + abstract killSessions(params: RR.KillSessionsReq): Promise // server - protected abstract setShareStatsRaw (params: RR.SetShareStatsReq): Promise - setShareStats = (params: RR.SetShareStatsReq) => this.syncResponse( - () => this.setShareStatsRaw(params), - )() + protected abstract setShareStatsRaw( + params: RR.SetShareStatsReq, + ): Promise + setShareStats = (params: RR.SetShareStatsReq) => + this.syncResponse(() => this.setShareStatsRaw(params))() - abstract getServerLogs (params: RR.GetServerLogsReq): Promise + abstract getServerLogs( + params: RR.GetServerLogsReq, + ): Promise - abstract getServerMetrics (params: RR.GetServerMetricsReq): Promise + abstract getServerMetrics( + params: RR.GetServerMetricsReq, + ): Promise - abstract getPkgMetrics (params: RR.GetPackageMetricsReq): Promise + abstract getPkgMetrics( + params: RR.GetPackageMetricsReq, + ): Promise - protected abstract updateServerRaw (params: RR.UpdateServerReq): Promise - updateServer = (params: RR.UpdateServerReq) => this.syncResponse( - () => this.updateServerWrapper(params), - )() - async updateServerWrapper (params: RR.UpdateServerReq) { + protected abstract updateServerRaw( + params: RR.UpdateServerReq, + ): Promise + updateServer = (params: RR.UpdateServerReq) => + this.syncResponse(() => this.updateServerWrapper(params))() + async updateServerWrapper(params: RR.UpdateServerReq) { const res = await this.updateServerRaw(params) if (res.response === 'no-updates') { throw new Error('Could ont find a newer version of EmbassyOS') @@ -64,23 +82,40 @@ export abstract class ApiService implements Source, Http { return res } - abstract restartServer (params: RR.UpdateServerReq): Promise + abstract restartServer( + params: RR.UpdateServerReq, + ): Promise - abstract shutdownServer (params: RR.ShutdownServerReq): Promise + abstract shutdownServer( + params: RR.ShutdownServerReq, + ): Promise - abstract systemRebuild (params: RR.SystemRebuildReq): Promise + abstract systemRebuild( + params: RR.SystemRebuildReq, + ): Promise // marketplace URLs - abstract getEos (params: RR.GetMarketplaceEOSReq): Promise + abstract getEos( + params: RR.GetMarketplaceEOSReq, + ): Promise - abstract getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise + abstract getMarketplaceData( + params: RR.GetMarketplaceDataReq, + url?: string, + ): Promise - abstract getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise + abstract getMarketplacePkgs( + params: RR.GetMarketplacePackagesReq, + ): Promise - abstract getReleaseNotes (params: RR.GetReleaseNotesReq): Promise + abstract getReleaseNotes( + params: RR.GetReleaseNotesReq, + ): Promise - abstract getLatestVersion (params: RR.GetLatestVersionReq): Promise + abstract getLatestVersion( + params: RR.GetLatestVersionReq, + ): Promise // protected abstract setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise // setPackageMarketplace = (params: RR.SetPackageMarketplaceReq) => this.syncResponse( @@ -92,112 +127,162 @@ export abstract class ApiService implements Source, Http { // notification - abstract getNotificationsRaw (params: RR.GetNotificationsReq): Promise - getNotifications = (params: RR.GetNotificationsReq) => this.syncResponse( - () => this.getNotificationsRaw(params), - )() + abstract getNotificationsRaw( + params: RR.GetNotificationsReq, + ): Promise + getNotifications = (params: RR.GetNotificationsReq) => + this.syncResponse(() => + this.getNotificationsRaw(params), + )() - abstract deleteNotification (params: RR.DeleteNotificationReq): Promise + abstract deleteNotification( + params: RR.DeleteNotificationReq, + ): Promise - abstract deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise + abstract deleteAllNotifications( + params: RR.DeleteAllNotificationsReq, + ): Promise // wifi - abstract getWifi (params: RR.GetWifiReq, timeout: number): Promise + abstract getWifi( + params: RR.GetWifiReq, + timeout: number, + ): Promise - abstract setWifiCountry (params: RR.SetWifiCountryReq): Promise + abstract setWifiCountry( + params: RR.SetWifiCountryReq, + ): Promise - abstract addWifi (params: RR.AddWifiReq): Promise + abstract addWifi(params: RR.AddWifiReq): Promise - abstract connectWifi (params: RR.ConnectWifiReq): Promise + abstract connectWifi(params: RR.ConnectWifiReq): Promise - abstract deleteWifi (params: RR.DeleteWifiReq): Promise + abstract deleteWifi(params: RR.DeleteWifiReq): Promise // ssh - abstract getSshKeys (params: RR.GetSSHKeysReq): Promise + abstract getSshKeys(params: RR.GetSSHKeysReq): Promise - abstract addSshKey (params: RR.AddSSHKeyReq): Promise + abstract addSshKey(params: RR.AddSSHKeyReq): Promise - abstract deleteSshKey (params: RR.DeleteSSHKeyReq): Promise + abstract deleteSshKey(params: RR.DeleteSSHKeyReq): Promise // backup - abstract getBackupTargets (params: RR.GetBackupTargetsReq): Promise + abstract getBackupTargets( + params: RR.GetBackupTargetsReq, + ): Promise - abstract addBackupTarget (params: RR.AddBackupTargetReq): Promise + abstract addBackupTarget( + params: RR.AddBackupTargetReq, + ): Promise - abstract updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise + abstract updateBackupTarget( + params: RR.UpdateBackupTargetReq, + ): Promise - abstract removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise + abstract removeBackupTarget( + params: RR.RemoveBackupTargetReq, + ): Promise - abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise + abstract getBackupInfo( + params: RR.GetBackupInfoReq, + ): Promise - protected abstract createBackupRaw (params: RR.CreateBackupReq): Promise - createBackup = (params: RR.CreateBackupReq) => this.syncResponse( - () => this.createBackupRaw(params), - )() + protected abstract createBackupRaw( + params: RR.CreateBackupReq, + ): Promise + createBackup = (params: RR.CreateBackupReq) => + this.syncResponse(() => this.createBackupRaw(params))() // package - abstract getPackageProperties (params: RR.GetPackagePropertiesReq): Promise['data']> + abstract getPackageProperties( + params: RR.GetPackagePropertiesReq, + ): Promise['data']> - abstract getPackageLogs (params: RR.GetPackageLogsReq): Promise + abstract getPackageLogs( + params: RR.GetPackageLogsReq, + ): Promise - protected abstract installPackageRaw (params: RR.InstallPackageReq): Promise - installPackage = (params: RR.InstallPackageReq) => this.syncResponse( - () => this.installPackageRaw(params), - )() + protected abstract installPackageRaw( + params: RR.InstallPackageReq, + ): Promise + installPackage = (params: RR.InstallPackageReq) => + this.syncResponse(() => this.installPackageRaw(params))() - abstract dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise + abstract dryUpdatePackage( + params: RR.DryUpdatePackageReq, + ): Promise - abstract getPackageConfig (params: RR.GetPackageConfigReq): Promise + abstract getPackageConfig( + params: RR.GetPackageConfigReq, + ): Promise - abstract drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise + abstract drySetPackageConfig( + params: RR.DrySetPackageConfigReq, + ): Promise - protected abstract setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise - setPackageConfig = (params: RR.SetPackageConfigReq) => this.syncResponse( - () => this.setPackageConfigRaw(params), - )() + protected abstract setPackageConfigRaw( + params: RR.SetPackageConfigReq, + ): Promise + setPackageConfig = (params: RR.SetPackageConfigReq) => + this.syncResponse(() => this.setPackageConfigRaw(params))() - protected abstract restorePackagesRaw (params: RR.RestorePackagesReq): Promise - restorePackages = (params: RR.RestorePackagesReq) => this.syncResponse( - () => this.restorePackagesRaw(params), - )() + protected abstract restorePackagesRaw( + params: RR.RestorePackagesReq, + ): Promise + restorePackages = (params: RR.RestorePackagesReq) => + this.syncResponse(() => this.restorePackagesRaw(params))() - abstract executePackageAction (params: RR.ExecutePackageActionReq): Promise + abstract executePackageAction( + params: RR.ExecutePackageActionReq, + ): Promise - protected abstract startPackageRaw (params: RR.StartPackageReq): Promise - startPackage = (params: RR.StartPackageReq) => this.syncResponse( - () => this.startPackageRaw(params), - )() + protected abstract startPackageRaw( + params: RR.StartPackageReq, + ): Promise + startPackage = (params: RR.StartPackageReq) => + this.syncResponse(() => this.startPackageRaw(params))() - abstract dryStopPackage (params: RR.DryStopPackageReq): Promise + abstract dryStopPackage( + params: RR.DryStopPackageReq, + ): Promise - protected abstract stopPackageRaw (params: RR.StopPackageReq): Promise - stopPackage = (params: RR.StopPackageReq) => this.syncResponse( - () => this.stopPackageRaw(params), - )() + protected abstract stopPackageRaw( + params: RR.StopPackageReq, + ): Promise + stopPackage = (params: RR.StopPackageReq) => + this.syncResponse(() => this.stopPackageRaw(params))() - abstract dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise + abstract dryUninstallPackage( + params: RR.DryUninstallPackageReq, + ): Promise - protected abstract uninstallPackageRaw (params: RR.UninstallPackageReq): Promise - uninstallPackage = (params: RR.UninstallPackageReq) => this.syncResponse( - () => this.uninstallPackageRaw(params), - )() + protected abstract uninstallPackageRaw( + params: RR.UninstallPackageReq, + ): Promise + uninstallPackage = (params: RR.UninstallPackageReq) => + this.syncResponse(() => this.uninstallPackageRaw(params))() - abstract dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise - - protected abstract deleteRecoveredPackageRaw (params: RR.UninstallPackageReq): Promise - deleteRecoveredPackage = (params: RR.UninstallPackageReq) => this.syncResponse( - () => this.deleteRecoveredPackageRaw(params), - )() + abstract dryConfigureDependency( + params: RR.DryConfigureDependencyReq, + ): Promise + protected abstract deleteRecoveredPackageRaw( + params: RR.UninstallPackageReq, + ): Promise + deleteRecoveredPackage = (params: RR.UninstallPackageReq) => + this.syncResponse(() => this.deleteRecoveredPackageRaw(params))() // Helper allowing quick decoration to sync the response patch and return the response contents. // Pass in a tempUpdate function which returns a UpdateTemp corresponding to a temporary // state change you'd like to enact prior to request and expired when request terminates. - private syncResponse Promise<{ response: T, revision?: Revision }>> (f: F, temp?: Operation): (...args: Parameters) => Promise { + private syncResponse< + T, + F extends (...args: any[]) => Promise<{ response: T; revision?: Revision }>, + >(f: F, temp?: Operation): (...args: Parameters) => Promise { return (...a) => { // let expireId = undefined // if (temp) { diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 2c4d4d230..12f9d5de5 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -1,20 +1,21 @@ import { Injectable } from '@angular/core' import { HttpService, Method } from '../http.service' -import { ApiService } from './embassy-api.service' +import { ApiService } from './embassy-api.service' import { RR } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' +import { PatchDbService } from '../patch-db/patch-db.service' @Injectable() export class LiveApiService extends ApiService { - - constructor ( + constructor( private readonly http: HttpService, + private readonly patch: PatchDbService, ) { - super(); - (window as any).rpcClient = this - } + super() + ;(window as any).rpcClient = this + } - async getStatic (url: string): Promise { + async getStatic(url: string): Promise { return this.http.httpRequest({ method: Method.GET, url, @@ -24,69 +25,101 @@ export class LiveApiService extends ApiService { // db - async getRevisions (since: number): Promise { + async getRevisions(since: number): Promise { return this.http.rpcRequest({ method: 'db.revisions', params: { since } }) } - async getDump (): Promise { + async getDump(): Promise { return this.http.rpcRequest({ method: 'db.dump' }) } - async setDbValueRaw (params: RR.SetDBValueReq): Promise { + async setDbValueRaw(params: RR.SetDBValueReq): Promise { return this.http.rpcRequest({ method: 'db.put.ui', params }) } // auth - async login (params: RR.LoginReq): Promise { + async login(params: RR.LoginReq): Promise { return this.http.rpcRequest({ method: 'auth.login', params }) } - async logout (params: RR.LogoutReq): Promise { + async logout(params: RR.LogoutReq): Promise { return this.http.rpcRequest({ method: 'auth.logout', params }) } - async getSessions (params: RR.GetSessionsReq): Promise { + async getSessions(params: RR.GetSessionsReq): Promise { return this.http.rpcRequest({ method: 'auth.session.list', params }) } - async killSessions (params: RR.KillSessionsReq): Promise { + async killSessions(params: RR.KillSessionsReq): Promise { return this.http.rpcRequest({ method: 'auth.session.kill', params }) } // server - async setShareStatsRaw (params: RR.SetShareStatsReq): Promise { - return this.http.rpcRequest( { method: 'server.config.share-stats', params }) + async setShareStatsRaw( + params: RR.SetShareStatsReq, + ): Promise { + return this.http.rpcRequest({ method: 'server.config.share-stats', params }) } - async getServerLogs (params: RR.GetServerLogsReq): Promise { - return this.http.rpcRequest( { method: 'server.logs', params }) + async getServerLogs( + params: RR.GetServerLogsReq, + ): Promise { + return this.http.rpcRequest({ method: 'server.logs', params }) } - async getServerMetrics (params: RR.GetServerMetricsReq): Promise { + async getServerMetrics( + params: RR.GetServerMetricsReq, + ): Promise { return this.http.rpcRequest({ method: 'server.metrics', params }) } - async updateServerRaw (params: RR.UpdateServerReq): Promise { + async updateServerRaw( + params: RR.UpdateServerReq, + ): Promise { return this.http.rpcRequest({ method: 'server.update', params }) } - async restartServer (params: RR.RestartServerReq): Promise { + async restartServer( + params: RR.RestartServerReq, + ): Promise { return this.http.rpcRequest({ method: 'server.restart', params }) } - async shutdownServer (params: RR.ShutdownServerReq): Promise { + async shutdownServer( + params: RR.ShutdownServerReq, + ): Promise { return this.http.rpcRequest({ method: 'server.shutdown', params }) } - async systemRebuild (params: RR.RestartServerReq): Promise { + async systemRebuild( + params: RR.RestartServerReq, + ): Promise { return this.http.rpcRequest({ method: 'server.rebuild', params }) } // marketplace URLs - async getEos (params: RR.GetMarketplaceEOSReq): Promise { + private async marketplaceProxy( + path: string, + params: {}, + url?: string, + ): Promise { + if (!url) { + const id = this.patch.data.ui.marketplace['selected-id'] + url = this.patch.data.ui.marketplace.options[id].url + } + const fullURL = `${url}${path}?${new URLSearchParams(params).toString()}` + return this.http.rpcRequest({ + method: 'marketplace.get', + params: { url: fullURL }, + }) + } + + async getEos( + params: RR.GetMarketplaceEOSReq, + ): Promise { return this.http.httpRequest({ method: Method.GET, url: '/marketplace/eos/latest', @@ -94,27 +127,26 @@ export class LiveApiService extends ApiService { }) } - async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise { - return this.http.httpRequest({ - method: Method.GET, - url: '/marketplace/package/data', - params, - }) + async getMarketplaceData( + params: RR.GetMarketplaceDataReq, + url?: string, + ): Promise { + return this.marketplaceProxy('/marketplace/package/data', params, url) } - async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise { + async getMarketplacePkgs( + params: RR.GetMarketplacePackagesReq, + ): Promise { if (params.query) params.category = undefined - return this.http.httpRequest({ - method: Method.GET, - url: '/marketplace/package/index', - params: { - ...params, - ids: JSON.stringify(params.ids), - }, + return this.marketplaceProxy('/marketplace/package/index', { + ...params, + ids: JSON.stringify(params.ids), }) } - async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise { + async getReleaseNotes( + params: RR.GetReleaseNotesReq, + ): Promise { return this.http.httpRequest({ method: Method.GET, url: '/marketplace/package/release-notes', @@ -122,7 +154,9 @@ export class LiveApiService extends ApiService { }) } - async getLatestVersion (params: RR.GetLatestVersionReq): Promise { + async getLatestVersion( + params: RR.GetLatestVersionReq, + ): Promise { return this.http.httpRequest({ method: Method.GET, url: '/marketplace/latest-version', @@ -141,149 +175,211 @@ export class LiveApiService extends ApiService { // notification - async getNotificationsRaw (params: RR.GetNotificationsReq): Promise { + async getNotificationsRaw( + params: RR.GetNotificationsReq, + ): Promise { return this.http.rpcRequest({ method: 'notification.list', params }) } - async deleteNotification (params: RR.DeleteNotificationReq): Promise { + async deleteNotification( + params: RR.DeleteNotificationReq, + ): Promise { return this.http.rpcRequest({ method: 'notification.delete', params }) } - async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise { - return this.http.rpcRequest({ method: 'notification.delete-before', params }) + async deleteAllNotifications( + params: RR.DeleteAllNotificationsReq, + ): Promise { + return this.http.rpcRequest({ + method: 'notification.delete-before', + params, + }) } // wifi - async getWifi (params: RR.GetWifiReq, timeout?: number): Promise { + async getWifi( + params: RR.GetWifiReq, + timeout?: number, + ): Promise { return this.http.rpcRequest({ method: 'wifi.get', params, timeout }) } - async setWifiCountry (params: RR.SetWifiCountryReq): Promise { + async setWifiCountry( + params: RR.SetWifiCountryReq, + ): Promise { return this.http.rpcRequest({ method: 'wifi.country.set', params }) } - async addWifi (params: RR.AddWifiReq): Promise { + async addWifi(params: RR.AddWifiReq): Promise { return this.http.rpcRequest({ method: 'wifi.add', params }) } - async connectWifi (params: RR.ConnectWifiReq): Promise { + async connectWifi(params: RR.ConnectWifiReq): Promise { return this.http.rpcRequest({ method: 'wifi.connect', params }) } - async deleteWifi (params: RR.DeleteWifiReq): Promise { + async deleteWifi(params: RR.DeleteWifiReq): Promise { return this.http.rpcRequest({ method: 'wifi.delete', params }) } // ssh - async getSshKeys (params: RR.GetSSHKeysReq): Promise { + async getSshKeys(params: RR.GetSSHKeysReq): Promise { return this.http.rpcRequest({ method: 'ssh.list', params }) } - async addSshKey (params: RR.AddSSHKeyReq): Promise { + async addSshKey(params: RR.AddSSHKeyReq): Promise { return this.http.rpcRequest({ method: 'ssh.add', params }) } - async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise { + async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise { return this.http.rpcRequest({ method: 'ssh.delete', params }) } // backup - async getBackupTargets (params: RR.GetBackupTargetsReq): Promise { + async getBackupTargets( + params: RR.GetBackupTargetsReq, + ): Promise { return this.http.rpcRequest({ method: 'backup.target.list', params }) } - async addBackupTarget (params: RR.AddBackupTargetReq): Promise { + async addBackupTarget( + params: RR.AddBackupTargetReq, + ): Promise { params.path = params.path.replace('/\\/g', '/') return this.http.rpcRequest({ method: 'backup.target.cifs.add', params }) } - async updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise { + async updateBackupTarget( + params: RR.UpdateBackupTargetReq, + ): Promise { return this.http.rpcRequest({ method: 'backup.target.cifs.update', params }) } - async removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise { + async removeBackupTarget( + params: RR.RemoveBackupTargetReq, + ): Promise { return this.http.rpcRequest({ method: 'backup.target.cifs.remove', params }) } - async getBackupInfo (params: RR.GetBackupInfoReq): Promise { + async getBackupInfo( + params: RR.GetBackupInfoReq, + ): Promise { return this.http.rpcRequest({ method: 'backup.target.info', params }) } - async createBackupRaw (params: RR.CreateBackupReq): Promise { + async createBackupRaw( + params: RR.CreateBackupReq, + ): Promise { return this.http.rpcRequest({ method: 'backup.create', params }) } // package - async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise ['data'] > { - return this.http.rpcRequest({ method: 'package.properties', params }) + async getPackageProperties( + params: RR.GetPackagePropertiesReq, + ): Promise['data']> { + return this.http + .rpcRequest({ method: 'package.properties', params }) .then(parsePropertiesPermissive) } - async getPackageLogs (params: RR.GetPackageLogsReq): Promise { - return this.http.rpcRequest( { method: 'package.logs', params }) + async getPackageLogs( + params: RR.GetPackageLogsReq, + ): Promise { + return this.http.rpcRequest({ method: 'package.logs', params }) } - async getPkgMetrics (params: RR.GetPackageMetricsReq): Promise { + async getPkgMetrics( + params: RR.GetPackageMetricsReq, + ): Promise { return this.http.rpcRequest({ method: 'package.metrics', params }) } - async installPackageRaw (params: RR.InstallPackageReq): Promise { + async installPackageRaw( + params: RR.InstallPackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.install', params }) } - async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise { + async dryUpdatePackage( + params: RR.DryUpdatePackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.update.dry', params }) } - async getPackageConfig (params: RR.GetPackageConfigReq): Promise { + async getPackageConfig( + params: RR.GetPackageConfigReq, + ): Promise { return this.http.rpcRequest({ method: 'package.config.get', params }) } - async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise { + async drySetPackageConfig( + params: RR.DrySetPackageConfigReq, + ): Promise { return this.http.rpcRequest({ method: 'package.config.set.dry', params }) } - async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise { + async setPackageConfigRaw( + params: RR.SetPackageConfigReq, + ): Promise { return this.http.rpcRequest({ method: 'package.config.set', params }) } - async restorePackagesRaw (params: RR.RestorePackagesReq): Promise { + async restorePackagesRaw( + params: RR.RestorePackagesReq, + ): Promise { return this.http.rpcRequest({ method: 'package.backup.restore', params }) } - async executePackageAction (params: RR.ExecutePackageActionReq): Promise { + async executePackageAction( + params: RR.ExecutePackageActionReq, + ): Promise { return this.http.rpcRequest({ method: 'package.action', params }) } - async startPackageRaw (params: RR.StartPackageReq): Promise { + async startPackageRaw( + params: RR.StartPackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.start', params }) } - async dryStopPackage (params: RR.DryStopPackageReq): Promise { + async dryStopPackage( + params: RR.DryStopPackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.stop.dry', params }) } - async stopPackageRaw (params: RR.StopPackageReq): Promise { + async stopPackageRaw(params: RR.StopPackageReq): Promise { return this.http.rpcRequest({ method: 'package.stop', params }) } - async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise { + async dryUninstallPackage( + params: RR.DryUninstallPackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.uninstall.dry', params }) } - async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise { + async deleteRecoveredPackageRaw( + params: RR.DeleteRecoveredPackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.delete-recovered', params }) } - async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise { + async uninstallPackageRaw( + params: RR.UninstallPackageReq, + ): Promise { return this.http.rpcRequest({ method: 'package.uninstall', params }) } - async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise { - return this.http.rpcRequest({ method: 'package.dependency.configure.dry', params }) + async dryConfigureDependency( + params: RR.DryConfigureDependencyReq, + ): Promise { + return this.http.rpcRequest({ + method: 'package.dependency.configure.dry', + params, + }) } } diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 5828a8804..1f2d0c0a5 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -25,22 +25,22 @@ export class MockApiService extends ApiService { private readonly revertTime = 4000 sequence: number - constructor(private readonly bootstrapper: LocalStorageBootstrap) { + constructor (private readonly bootstrapper: LocalStorageBootstrap) { super() } - async getStatic(url: string): Promise { + async getStatic (url: string): Promise { await pauseFor(2000) return markdown } // db - async getRevisions(since: number): Promise { + async getRevisions (since: number): Promise { return this.getDump() } - async getDump(): Promise { + async getDump (): Promise { const cache = await this.bootstrapper.init() return { id: cache.sequence, @@ -49,7 +49,7 @@ export class MockApiService extends ApiService { } } - async setDbValueRaw(params: RR.SetDBValueReq): Promise { + async setDbValueRaw (params: RR.SetDBValueReq): Promise { await pauseFor(2000) const patch = [ { @@ -63,7 +63,7 @@ export class MockApiService extends ApiService { // auth - async login(params: RR.LoginReq): Promise { + async login (params: RR.LoginReq): Promise { await pauseFor(2000) setTimeout(() => { @@ -73,24 +73,24 @@ export class MockApiService extends ApiService { return null } - async logout(params: RR.LogoutReq): Promise { + async logout (params: RR.LogoutReq): Promise { await pauseFor(2000) return null } - async getSessions(params: RR.GetSessionsReq): Promise { + async getSessions (params: RR.GetSessionsReq): Promise { await pauseFor(2000) return Mock.Sessions } - async killSessions(params: RR.KillSessionsReq): Promise { + async killSessions (params: RR.KillSessionsReq): Promise { await pauseFor(2000) return null } // server - async setShareStatsRaw( + async setShareStatsRaw ( params: RR.SetShareStatsReq, ): Promise { await pauseFor(2000) @@ -105,7 +105,7 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async getServerLogs( + async getServerLogs ( params: RR.GetServerLogsReq, ): Promise { await pauseFor(2000) @@ -127,21 +127,21 @@ export class MockApiService extends ApiService { } } - async getServerMetrics( + async getServerMetrics ( params: RR.GetServerMetricsReq, ): Promise { await pauseFor(2000) return Mock.getServerMetrics() } - async getPkgMetrics( + async getPkgMetrics ( params: RR.GetServerMetricsReq, ): Promise { await pauseFor(2000) return Mock.getAppMetrics() } - async updateServerRaw( + async updateServerRaw ( params: RR.UpdateServerReq, ): Promise { await pauseFor(2000) @@ -165,21 +165,21 @@ export class MockApiService extends ApiService { return this.withRevision(patch, 'updating') } - async restartServer( + async restartServer ( params: RR.RestartServerReq, ): Promise { await pauseFor(2000) return null } - async shutdownServer( + async shutdownServer ( params: RR.ShutdownServerReq, ): Promise { await pauseFor(2000) return null } - async systemRebuild( + async systemRebuild ( params: RR.RestartServerReq, ): Promise { await pauseFor(2000) @@ -188,18 +188,20 @@ export class MockApiService extends ApiService { // marketplace URLs - async getEos( + async getEos ( params: RR.GetMarketplaceEOSReq, ): Promise { await pauseFor(2000) return Mock.MarketplaceEos } - async getMarketplaceData( + async getMarketplaceData ( params: RR.GetMarketplaceDataReq, + url?: string, ): Promise { await pauseFor(2000) return { + name: 'Dark9', categories: [ 'featured', 'bitcoin', @@ -212,21 +214,21 @@ export class MockApiService extends ApiService { } } - async getMarketplacePkgs( + async getMarketplacePkgs ( params: RR.GetMarketplacePackagesReq, ): Promise { await pauseFor(2000) return Mock.MarketplacePkgsList } - async getReleaseNotes( + async getReleaseNotes ( params: RR.GetReleaseNotesReq, ): Promise { await pauseFor(2000) return Mock.ReleaseNotes } - async getLatestVersion( + async getLatestVersion ( params: RR.GetLatestVersionReq, ): Promise { await pauseFor(2000) @@ -244,7 +246,7 @@ export class MockApiService extends ApiService { // notification - async getNotificationsRaw( + async getNotificationsRaw ( params: RR.GetNotificationsReq, ): Promise { await pauseFor(2000) @@ -259,14 +261,14 @@ export class MockApiService extends ApiService { return this.withRevision(patch, Mock.Notifications) } - async deleteNotification( + async deleteNotification ( params: RR.DeleteNotificationReq, ): Promise { await pauseFor(2000) return null } - async deleteAllNotifications( + async deleteAllNotifications ( params: RR.DeleteAllNotificationsReq, ): Promise { await pauseFor(2000) @@ -275,60 +277,60 @@ export class MockApiService extends ApiService { // wifi - async getWifi(params: RR.GetWifiReq): Promise { + async getWifi (params: RR.GetWifiReq): Promise { await pauseFor(2000) return Mock.Wifi } - async setWifiCountry( + async setWifiCountry ( params: RR.SetWifiCountryReq, ): Promise { await pauseFor(2000) return null } - async addWifi(params: RR.AddWifiReq): Promise { + async addWifi (params: RR.AddWifiReq): Promise { await pauseFor(2000) return null } - async connectWifi(params: RR.ConnectWifiReq): Promise { + async connectWifi (params: RR.ConnectWifiReq): Promise { await pauseFor(2000) return null } - async deleteWifi(params: RR.DeleteWifiReq): Promise { + async deleteWifi (params: RR.DeleteWifiReq): Promise { await pauseFor(2000) return null } // ssh - async getSshKeys(params: RR.GetSSHKeysReq): Promise { + async getSshKeys (params: RR.GetSSHKeysReq): Promise { await pauseFor(2000) return Mock.SshKeys } - async addSshKey(params: RR.AddSSHKeyReq): Promise { + async addSshKey (params: RR.AddSSHKeyReq): Promise { await pauseFor(2000) return Mock.SshKey } - async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise { + async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise { await pauseFor(2000) return null } // backup - async getBackupTargets( + async getBackupTargets ( params: RR.GetBackupTargetsReq, ): Promise { await pauseFor(2000) return Mock.BackupTargets } - async addBackupTarget( + async addBackupTarget ( params: RR.AddBackupTargetReq, ): Promise { await pauseFor(2000) @@ -345,7 +347,7 @@ export class MockApiService extends ApiService { } } - async updateBackupTarget( + async updateBackupTarget ( params: RR.UpdateBackupTargetReq, ): Promise { await pauseFor(2000) @@ -360,21 +362,21 @@ export class MockApiService extends ApiService { } } - async removeBackupTarget( + async removeBackupTarget ( params: RR.RemoveBackupTargetReq, ): Promise { await pauseFor(2000) return null } - async getBackupInfo( + async getBackupInfo ( params: RR.GetBackupInfoReq, ): Promise { await pauseFor(2000) return Mock.BackupInfo } - async createBackupRaw( + async createBackupRaw ( params: RR.CreateBackupReq, ): Promise { await pauseFor(2000) @@ -425,14 +427,14 @@ export class MockApiService extends ApiService { // package - async getPackageProperties( + async getPackageProperties ( params: RR.GetPackagePropertiesReq, ): Promise['data']> { await pauseFor(2000) return parsePropertiesPermissive(Mock.PackageProperties) } - async getPackageLogs( + async getPackageLogs ( params: RR.GetPackageLogsReq, ): Promise { await pauseFor(2000) @@ -454,7 +456,7 @@ export class MockApiService extends ApiService { } } - async installPackageRaw( + async installPackageRaw ( params: RR.InstallPackageReq, ): Promise { await pauseFor(2000) @@ -489,14 +491,14 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async dryUpdatePackage( + async dryUpdatePackage ( params: RR.DryUpdatePackageReq, ): Promise { await pauseFor(2000) return {} } - async getPackageConfig( + async getPackageConfig ( params: RR.GetPackageConfigReq, ): Promise { await pauseFor(2000) @@ -506,14 +508,14 @@ export class MockApiService extends ApiService { } } - async drySetPackageConfig( + async drySetPackageConfig ( params: RR.DrySetPackageConfigReq, ): Promise { await pauseFor(2000) return {} } - async setPackageConfigRaw( + async setPackageConfigRaw ( params: RR.SetPackageConfigReq, ): Promise { await pauseFor(2000) @@ -527,7 +529,7 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async restorePackagesRaw( + async restorePackagesRaw ( params: RR.RestorePackagesReq, ): Promise { await pauseFor(2000) @@ -563,14 +565,14 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async executePackageAction( + async executePackageAction ( params: RR.ExecutePackageActionReq, ): Promise { await pauseFor(2000) return Mock.ActionResponse } - async startPackageRaw( + async startPackageRaw ( params: RR.StartPackageReq, ): Promise { const path = `/package-data/${params.id}/installed/status/main` @@ -649,7 +651,7 @@ export class MockApiService extends ApiService { return this.withRevision(originalPatch) } - async dryStopPackage( + async dryStopPackage ( params: RR.DryStopPackageReq, ): Promise { await pauseFor(2000) @@ -663,7 +665,7 @@ export class MockApiService extends ApiService { } } - async stopPackageRaw(params: RR.StopPackageReq): Promise { + async stopPackageRaw (params: RR.StopPackageReq): Promise { await pauseFor(2000) const path = `/package-data/${params.id}/installed/status/main` @@ -694,14 +696,14 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async dryUninstallPackage( + async dryUninstallPackage ( params: RR.DryUninstallPackageReq, ): Promise { await pauseFor(2000) return {} } - async uninstallPackageRaw( + async uninstallPackageRaw ( params: RR.UninstallPackageReq, ): Promise { await pauseFor(2000) @@ -727,7 +729,7 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async deleteRecoveredPackageRaw( + async deleteRecoveredPackageRaw ( params: RR.DeleteRecoveredPackageReq, ): Promise { await pauseFor(2000) @@ -740,7 +742,7 @@ export class MockApiService extends ApiService { return this.withRevision(patch) } - async dryConfigureDependency( + async dryConfigureDependency ( params: RR.DryConfigureDependencyReq, ): Promise { await pauseFor(2000) @@ -751,7 +753,7 @@ export class MockApiService extends ApiService { } } - private async updateProgress( + private async updateProgress ( id: string, initialProgress: InstallProgress, ): Promise { @@ -797,7 +799,7 @@ export class MockApiService extends ApiService { }, 1000) } - private async updateOSProgress(size: number) { + private async updateOSProgress (size: number) { let downloaded = 0 while (downloaded < size) { await pauseFor(250) @@ -847,7 +849,7 @@ export class MockApiService extends ApiService { }, 1000) } - private async updateMock(patch: Operation[]): Promise { + private async updateMock (patch: Operation[]): Promise { if (!this.sequence) { const { sequence } = await this.bootstrapper.init() this.sequence = sequence @@ -860,7 +862,7 @@ export class MockApiService extends ApiService { this.mockPatch$.next(revision) } - private async withRevision( + private async withRevision ( patch: Operation[], response: T = null, ): Promise> { diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index f3088b1d9..a3ad403a0 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -16,6 +16,15 @@ export const mockPatchData: DataModel = { 'pkg-order': [], 'ack-welcome': '1.0.0', 'ack-share-stats': false, + marketplace: { + 'selected-id': 'asdfasdf', + options: { + asdfasdf: { + name: 'Start9', + url: 'start9marketplace.com', + }, + }, + }, }, 'server-info': { id: 'embassy-abcdefgh', diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 2bdc11050..3f1956e4e 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -13,6 +13,17 @@ export interface UIData { 'pkg-order': string[] 'ack-welcome': string // EOS version 'ack-share-stats': boolean + marketplace: UIMarketplaceData +} + +export interface UIMarketplaceData { + 'selected-id': string + options: { + [id: string]: { + url: string + name: string + } + } } export interface ServerInfo { diff --git a/frontend/projects/ui/src/app/services/startup-alerts.service.ts b/frontend/projects/ui/src/app/services/startup-alerts.service.ts index 892e37918..3028e9ff7 100644 --- a/frontend/projects/ui/src/app/services/startup-alerts.service.ts +++ b/frontend/projects/ui/src/app/services/startup-alerts.service.ts @@ -1,5 +1,10 @@ import { Injectable } from '@angular/core' -import { AlertController, IonicSafeString, ModalController, NavController } from '@ionic/angular' +import { + AlertController, + IonicSafeString, + ModalController, + NavController, +} from '@ionic/angular' import { wizardModal } from '../components/install-wizard/install-wizard.component' import { WizardBaker } from '../components/install-wizard/prebaked-wizards' import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page' @@ -15,6 +20,7 @@ import { isEmptyObject } from '../util/misc.util' import { ApiService } from './api/embassy-api.service' import { Subscription } from 'rxjs' import { ServerConfigService } from './server-config.service' +import { v4 } from 'uuid' @Injectable({ providedIn: 'root', @@ -22,7 +28,7 @@ import { ServerConfigService } from './server-config.service' export class StartupAlertsService { private checks: Check[] - constructor ( + constructor( private readonly alertCtrl: AlertController, private readonly navCtrl: NavController, private readonly config: ConfigService, @@ -52,69 +58,74 @@ export class StartupAlertsService { check: () => this.osUpdateCheck(), display: pkg => this.displayOsUpdateCheck(pkg), } - const pkgsUpdate: Check = { - name: 'pkgsUpdate', - shouldRun: () => this.shouldRunAppsCheck(), - check: () => this.appsCheck(), - display: () => this.displayAppsCheck(), - } - this.checks = [osWelcome, shareStats, osUpdate, pkgsUpdate] + this.checks = [osWelcome, shareStats, osUpdate] } // This takes our three checks and filters down to those that should run. // Then, the reduce fires, quickly iterating through yielding a promise (previousDisplay) to the next element // Each promise fires more or less concurrently, so each c.check(server) is run concurrently // Then, since we await previousDisplay before c.display(res), each promise executing gets hung awaiting the display of the previous run - runChecks (): Subscription { - return this.patch.watch$() - .pipe( - filter(data => !isEmptyObject(data)), - take(1), - ) - .subscribe(async () => { - await this.checks - .filter(c => !this.config.skipStartupAlerts && c.shouldRun()) - // returning true in the below block means to continue to next modal - // returning false means to skip all subsequent modals - .reduce(async (previousDisplay, c) => { - let checkRes: any - try { - checkRes = await c.check() - } catch (e) { - console.error(`Exception in ${c.name} check:`, e) - return true + runChecks(): Subscription { + return this.patch + .watch$() + .pipe( + filter(data => !isEmptyObject(data)), + take(1), + ) + .subscribe(async data => { + if (!data.ui.marketplace) { + const uuid = v4() + const value = { + 'selected-id': uuid, + options: { + [uuid]: { + url: 'marketplaceurl.com', + name: 'Start9', + }, + }, + } + await this.api.setDbValue({ pointer: 'marketplace', value }) } + await this.checks + .filter(c => !this.config.skipStartupAlerts && c.shouldRun()) + // returning true in the below block means to continue to next modal + // returning false means to skip all subsequent modals + .reduce(async (previousDisplay, c) => { + let checkRes: any + try { + checkRes = await c.check() + } catch (e) { + console.error(`Exception in ${c.name} check:`, e) + return true + } - const displayRes = await previousDisplay + const displayRes = await previousDisplay - if (!checkRes) return true - if (displayRes) return c.display(checkRes) - }, Promise.resolve(true)) - }) + if (!checkRes) return true + if (displayRes) return c.display(checkRes) + }, Promise.resolve(true)) + }) } // ** should run ** - private shouldRunOsWelcome (): boolean { + private shouldRunOsWelcome(): boolean { return this.patch.getData().ui['ack-welcome'] !== this.config.version } - private shouldRunShareStats (): boolean { + private shouldRunShareStats(): boolean { return !this.patch.getData().ui['ack-share-stats'] } - private shouldRunOsUpdateCheck (): boolean { - return this.patch.getData().ui['auto-check-updates'] - } - - private shouldRunAppsCheck (): boolean { + private shouldRunOsUpdateCheck(): boolean { return this.patch.getData().ui['auto-check-updates'] } // ** check ** - private async osUpdateCheck (): Promise { + private async osUpdateCheck(): Promise { const res = await this.api.getEos({ - 'eos-version-compat': this.patch.getData()['server-info']['eos-version-compat'], + 'eos-version-compat': + this.patch.getData()['server-info']['eos-version-compat'], }) if (this.emver.compare(this.config.version, res.version) === -1) { @@ -124,14 +135,9 @@ export class StartupAlertsService { } } - private async appsCheck (): Promise { - const updates = await this.marketplaceService.getUpdates(this.patch.getData()['package-data']) - return !!updates.length - } - // ** display ** - private async displayOsWelcome (): Promise { + private async displayOsWelcome(): Promise { return new Promise(async resolve => { const modal = await this.modalCtrl.create({ component: OSWelcomePage, @@ -141,27 +147,37 @@ export class StartupAlertsService { }, }) modal.onWillDismiss().then(() => { - this.api.setDbValue({ pointer: '/ack-welcome', value: this.config.version }) - .catch() + this.api + .setDbValue({ pointer: '/ack-welcome', value: this.config.version }) + .catch() return resolve(true) }) await modal.present() }) } - private async displayShareStats (): Promise { + private async displayShareStats(): Promise { return new Promise(async resolve => { - const alert = await this.serverConfig.presentAlert('share-stats', this.patch.getData()['server-info']['share-stats']) + const alert = await this.serverConfig.presentAlert( + 'share-stats', + this.patch.getData()['server-info']['share-stats'], + ) alert.onDidDismiss().then(() => { - this.api.setDbValue({ pointer: '/ack-share-stats', value: this.config.version }) + this.api + .setDbValue({ + pointer: '/ack-share-stats', + value: this.config.version, + }) .catch() return resolve(true) }) }) } - private async displayOsUpdateCheck (eos: RR.GetMarketplaceEOSRes): Promise { + private async displayOsUpdateCheck( + eos: RR.GetMarketplaceEOSRes, + ): Promise { const { update } = await this.presentAlertNewOS(eos.version) if (update) { const { cancelled } = await wizardModal( @@ -178,46 +194,19 @@ export class StartupAlertsService { return true } - private async displayAppsCheck (): Promise { - return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Updates Available!', - message: new IonicSafeString( - `
-
New service updates are available in the Marketplace.
-
You can disable these checks in your Embassy Config
-
- `, - ), - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => resolve(true), - }, - { - text: 'View in Marketplace', - handler: () => { - this.navCtrl.navigateForward('/marketplace').then(() => resolve(false)) - }, - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() - }) - } - // more - private async presentAlertNewOS (versionLatest: string): Promise<{ cancel?: true, update?: true }> { + private async presentAlertNewOS( + versionLatest: string, + ): Promise<{ cancel?: true; update?: true }> { return new Promise(async resolve => { const alert = await this.alertCtrl.create({ header: 'New EmbassyOS Version!', message: new IonicSafeString( `
-
Update EmbassyOS to version ${displayEmver(versionLatest)}?
+
Update EmbassyOS to version ${displayEmver( + versionLatest, + )}?
You can disable these checks in your Embassy Config
`, @@ -250,4 +239,4 @@ type Check = { display: (a: T) => Promise // for logging purposes name: string -} \ No newline at end of file +}