From 7213d82f1b36f01a3d6ae0caf70d460bd4cb3255 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 31 May 2023 12:29:48 -0600 Subject: [PATCH] Feat/credentials (#2290) add credentials and remove properties --- .../app/app/preloader/preloader.component.ts | 1 - .../app-credentials.module.ts} | 10 +- .../app-credentials/app-credentials.page.html | 58 +++++++ .../app-credentials.page.scss} | 0 .../app-credentials/app-credentials.page.ts | 72 ++++++++ .../app-properties/app-properties.page.html | 119 -------------- .../app-properties/app-properties.page.ts | 154 ------------------ .../app-show/pipes/to-buttons.pipe.ts | 11 +- .../pages/apps-routes/apps-routing.module.ts | 6 +- .../ui/src/app/services/api/api.types.ts | 6 +- .../app/services/api/embassy-api.service.ts | 6 +- .../services/api/embassy-live-api.service.ts | 11 +- .../services/api/embassy-mock-api.service.ts | 12 +- .../ui/src/app/util/properties.util.ts | 152 ----------------- 14 files changed, 159 insertions(+), 459 deletions(-) rename frontend/projects/ui/src/app/apps/ui/pages/apps-routes/{app-properties/app-properties.module.ts => app-credentials/app-credentials.module.ts} (67%) create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.html rename frontend/projects/ui/src/app/apps/ui/pages/apps-routes/{app-properties/app-properties.page.scss => app-credentials/app-credentials.page.scss} (100%) create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.ts delete mode 100644 frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.html delete mode 100644 frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.ts delete mode 100644 frontend/projects/ui/src/app/util/properties.util.ts diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts index 2dbb23e27..df0ae29b7 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -11,7 +11,6 @@ const ICONS = [ 'arrow-back', 'arrow-forward', 'arrow-up', - 'briefcase-outline', 'brush-outline', 'bookmark-outline', 'cellular-outline', diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.module.ts similarity index 67% rename from frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.module.ts rename to frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.module.ts index 2d8553017..ac57600af 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.module.ts @@ -2,18 +2,19 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { AppPropertiesPage } from './app-properties.page' +import { AppCredentialsPage } from './app-credentials.page' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { MaskPipeModule } from 'src/app/pipes/mask/mask.module' import { SharedPipesModule, TextSpinnerComponentModule, } from '@start9labs/shared' +import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' const routes: Routes = [ { path: '', - component: AppPropertiesPage, + component: AppCredentialsPage, }, ] @@ -26,7 +27,8 @@ const routes: Routes = [ SharedPipesModule, TextSpinnerComponentModule, MaskPipeModule, + SkeletonListComponentModule, ], - declarations: [AppPropertiesPage], + declarations: [AppCredentialsPage], }) -export class AppPropertiesPageModule {} +export class AppCredentialsPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.html b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.html new file mode 100644 index 000000000..151a24396 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.html @@ -0,0 +1,58 @@ + + + + + + Credentials + + + + Refresh + + + + + + + + + + + + + + +

No credentials

+
+
+ + + + + +

{{ cred.key }}

+

+ {{ unmasked[cred.key] ? cred.value : (cred.value | mask : 64) }} +

+
+
+ + + + + + +
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.scss similarity index 100% rename from frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.scss rename to frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.scss diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.ts new file mode 100644 index 000000000..e50890b83 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-credentials/app-credentials.page.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ToastController } from '@ionic/angular' +import { + ErrorToastService, + getPkgId, + copyToClipboard, + pauseFor, +} from '@start9labs/shared' + +@Component({ + selector: 'app-credentials', + templateUrl: './app-credentials.page.html', + styleUrls: ['./app-credentials.page.scss'], +}) +export class AppCredentialsPage { + readonly pkgId = getPkgId(this.route) + credentials: Record = {} + unmasked: { [key: string]: boolean } = {} + loading = true + + constructor( + private readonly route: ActivatedRoute, + private readonly embassyApi: ApiService, + private readonly errToast: ErrorToastService, + private readonly toastCtrl: ToastController, + ) {} + + async ngOnInit() { + await this.getCredentials() + } + + async refresh() { + await this.getCredentials() + } + + async copy(text: string): Promise { + const success = await copyToClipboard(text) + const message = success + ? 'Copied. Clearing clipboard in 20 seconds' + : 'Failed to copy.' + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 2000, + }) + await toast.present() + } + + toggleMask(key: string) { + this.unmasked[key] = !this.unmasked[key] + } + + private async getCredentials(): Promise { + this.loading = true + try { + this.credentials = await this.embassyApi.getPackageCredentials({ + id: this.pkgId, + }) + } catch (e: any) { + this.errToast.present(e) + } finally { + this.loading = false + } + } + + asIsOrder(a: any, b: any) { + return 0 + } +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.html b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.html deleted file mode 100644 index ca3cdd3be..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - Properties - - - - Refresh - - - - - - - - - - - - -

- - Service is stopped. Information on this page could be inaccurate. - -

-
-
- - - - -

No properties.

-
-
- - - -
- - - - - - -

{{ prop.key }}

-
-
- - - - - - -

{{ prop.key }}

-

- {{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | - mask : 64) : prop.value.value }} -

-
-
- - - - - - - - - -
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.ts deleted file mode 100644 index 8a49bad52..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-properties/app-properties.page.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Component, ViewChild } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - AlertController, - IonBackButtonDelegate, - ModalController, - NavController, - ToastController, -} from '@ionic/angular' -import { PackageProperties } from 'src/app/util/properties.util' -import { QRComponent } from 'src/app/components/qr/qr.component' -import { PatchDB } from 'patch-db-client' -import { - DataModel, - PackageMainStatus, -} from 'src/app/services/patch-db/data-model' -import { - ErrorToastService, - getPkgId, - copyToClipboard, -} from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' -import { getValueByPointer } from 'fast-json-patch' -import { map, takeUntil } from 'rxjs/operators' - -@Component({ - selector: 'app-properties', - templateUrl: './app-properties.page.html', - styleUrls: ['./app-properties.page.scss'], - providers: [TuiDestroyService], -}) -export class AppPropertiesPage { - loading = true - readonly pkgId = getPkgId(this.route) - - pointer = '' - node: PackageProperties = {} - - properties: PackageProperties = {} - unmasked: { [key: string]: boolean } = {} - - stopped$ = this.patch - .watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status') - .pipe(map(status => status === PackageMainStatus.Stopped)) - - @ViewChild(IonBackButtonDelegate, { static: false }) - backButton?: IonBackButtonDelegate - - constructor( - private readonly route: ActivatedRoute, - private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, - private readonly toastCtrl: ToastController, - private readonly modalCtrl: ModalController, - private readonly navCtrl: NavController, - private readonly patch: PatchDB, - private readonly destroy$: TuiDestroyService, - ) {} - - ionViewDidEnter() { - if (!this.backButton) return - this.backButton.onClick = () => { - history.back() - } - } - - async ngOnInit() { - await this.getProperties() - - this.route.queryParams - .pipe(takeUntil(this.destroy$)) - .subscribe(queryParams => { - if (queryParams['pointer'] === this.pointer) return - this.pointer = queryParams['pointer'] || '' - this.node = getValueByPointer(this.properties, this.pointer) - }) - } - - async refresh() { - await this.getProperties() - } - - async presentDescription( - property: { key: string; value: PackageProperties[''] }, - e: Event, - ) { - e.stopPropagation() - - const alert = await this.alertCtrl.create({ - header: property.key, - message: property.value.description || undefined, - }) - await alert.present() - } - - async goToNested(key: string): Promise { - this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, { - queryParams: { - pointer: `${this.pointer}/${key}/value`, - }, - }) - } - - async copy(text: string): Promise { - let message = '' - await copyToClipboard(text).then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async showQR(text: string): Promise { - const modal = await this.modalCtrl.create({ - component: QRComponent, - componentProps: { - text, - }, - cssClass: 'qr-modal', - }) - await modal.present() - } - - toggleMask(key: string) { - this.unmasked[key] = !this.unmasked[key] - } - - private async getProperties(): Promise { - this.loading = true - try { - this.properties = await this.embassyApi.getPackageProperties({ - id: this.pkgId, - }) - this.node = getValueByPointer(this.properties, this.pointer) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.loading = false - } - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index bc73edc16..8b97302e2 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -62,16 +62,15 @@ export class ToButtonsPipe implements PipeTransform { description: `Customize ${pkgTitle}`, icon: 'options-outline', }, - // properties + // credentials { action: () => - this.navCtrl.navigateForward(['properties'], { + this.navCtrl.navigateForward(['credentials'], { relativeTo: this.route, }), - title: 'Properties', - description: - 'Runtime information, credentials, and other values of interest', - icon: 'briefcase-outline', + title: 'Credentials', + description: 'Password, keys, or other credentials of interest', + icon: 'key-outline', }, // actions { diff --git a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/apps-routing.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/apps-routing.module.ts index 21d1f9dea..f5343bcc2 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/apps-routing.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/apps-routes/apps-routing.module.ts @@ -37,10 +37,10 @@ const routes: Routes = [ import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), }, { - path: ':pkgId/properties', + path: ':pkgId/credentials', loadChildren: () => - import('./app-properties/app-properties.module').then( - m => m.AppPropertiesPageModule, + import('./app-credentials/app-credentials.module').then( + m => m.AppCredentialsPageModule, ), }, ] 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 d39d3a0b9..8ec0ecf59 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -1,6 +1,5 @@ import { Dump, Revision } from 'patch-db-client' import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace' -import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { DataModel, @@ -235,9 +234,8 @@ export module RR { // package - export type GetPackagePropertiesReq = { id: string } // package.properties - export type GetPackagePropertiesRes = - PackagePropertiesVersioned + export type GetPackageCredentialsReq = { id: string } // package.credentials + export type GetPackageCredentialsRes = Record export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs export type GetPackageLogsRes = LogsRes 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 2b149e919..168a7692b 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 @@ -234,9 +234,9 @@ export abstract class ApiService { // package - abstract getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> + abstract getPackageCredentials( + params: RR.GetPackageCredentialsReq, + ): Promise abstract getPackageLogs( params: RR.GetPackageLogsReq, 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 2194ac0e3..3fb8f5a47 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 @@ -12,7 +12,6 @@ import { } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { BackupTargetType, Metrics, RR } from './api.types' -import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { ConfigService } from '../config.service' import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' import { Observable } from 'rxjs' @@ -405,12 +404,10 @@ export class LiveApiService extends ApiService { // package - async getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> { - return this.rpcRequest({ method: 'package.properties', params }).then( - parsePropertiesPermissive, - ) + async getPackageCredentials( + params: RR.GetPackageCredentialsReq, + ): Promise { + return this.rpcRequest({ method: 'package.credentials', params }) } async getPackageLogs( 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 32e74e779..a94564b74 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 @@ -17,7 +17,6 @@ import { PackageState, } from 'src/app/services/patch-db/data-model' import { BackupTargetType, Metrics, RR } from './api.types' -import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' import { @@ -667,12 +666,13 @@ export class MockApiService extends ApiService { // package - async getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> { + async getPackageCredentials( + params: RR.GetPackageCredentialsReq, + ): Promise { await pauseFor(2000) - return '' as any - // return parsePropertiesPermissive(Mock.PackageProperties) + return { + password: 'specialPassword$', + } } async getPackageLogs( diff --git a/frontend/projects/ui/src/app/util/properties.util.ts b/frontend/projects/ui/src/app/util/properties.util.ts deleted file mode 100644 index 0d1d7e6f4..000000000 --- a/frontend/projects/ui/src/app/util/properties.util.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { applyOperation } from 'fast-json-patch' -import matches, { - Parser, - shape, - string, - literal, - boolean, - deferred, - dictionary, - anyOf, - number, - arrayOf, -} from 'ts-matches' - -type ValidVersion = 1 | 2 - -type PropertiesV1 = typeof matchPropertiesV1._TYPE -type PackagePropertiesV1 = PropertiesV1[] -type PackagePropertiesV2 = { - [name: string]: PackagePropertyString | PackagePropertyObject -} -type PackagePropertiesVersionedData = T extends 1 - ? PackagePropertiesV1 - : T extends 2 - ? PackagePropertiesV2 - : never - -type PackagePropertyString = typeof matchPackagePropertyString._TYPE - -export type PackagePropertiesVersioned = { - version: T - data: PackagePropertiesVersionedData -} -export type PackageProperties = PackagePropertiesV2 - -const matchPropertiesV1 = shape( - { - name: string, - value: string, - description: string, - copyable: boolean, - qr: boolean, - }, - ['description', 'copyable', 'qr'], - { copyable: false, qr: false } as const, -) - -const [matchPackagePropertiesV2, setPPV2] = deferred() -const matchPackagePropertyString = shape( - { - type: literal('string'), - description: string, - value: string, - copyable: boolean, - qr: boolean, - masked: boolean, - }, - ['description', 'copyable', 'qr', 'masked'], - { - copyable: false, - qr: false, - masked: false, - } as const, -) -const matchPackagePropertyObject = shape( - { - type: literal('object'), - value: matchPackagePropertiesV2, - description: string, - }, - ['description'], -) - -const matchPropertyV2 = anyOf( - matchPackagePropertyString, - matchPackagePropertyObject, -) -type PackagePropertyObject = typeof matchPackagePropertyObject._TYPE -setPPV2(dictionary([string, matchPropertyV2])) - -const matchPackagePropertiesVersionedV1 = shape({ - version: number, - data: arrayOf(matchPropertiesV1), -}) -const matchPackagePropertiesVersionedV2 = shape({ - version: number, - data: dictionary([string, matchPropertyV2]), -}) - -export function parsePropertiesPermissive( - properties: unknown, - errorCallback: (err: Error) => any = console.warn, -): PackageProperties { - return matches(properties) - .when(matchPackagePropertiesVersionedV1, prop => - parsePropertiesV1Permissive(prop.data, errorCallback), - ) - .when(matchPackagePropertiesVersionedV2, prop => prop.data) - .when(matches.nill, {}) - .defaultToLazy(() => { - errorCallback(new TypeError(`value is not valid`)) - return {} - }) -} - -function parsePropertiesV1Permissive( - properties: unknown, - errorCallback: (err: Error) => any, -): PackageProperties { - if (!Array.isArray(properties)) { - errorCallback(new TypeError(`${properties} is not an array`)) - return {} - } - return properties.reduce( - (prev: PackagePropertiesV2, cur: unknown, idx: number) => { - const result = matchPropertiesV1.enumParsed(cur) - if ('value' in result) { - const value = result.value - prev[value.name] = { - type: 'string', - value: value.value, - description: value.description, - copyable: value.copyable, - qr: value.qr, - masked: false, - } - } else { - const error = result.error - const message = Parser.validatorErrorAsString(error) - const dataPath = error.keys.map(removeQuotes).join('/') - errorCallback(new Error(`/data/${idx}: ${message}`)) - if (dataPath) { - applyOperation(cur, { - op: 'replace', - path: `/${dataPath}`, - value: undefined, - }) - } - } - return prev - }, - {}, - ) -} - -const removeRegex = /('|")/ -function removeQuotes(x: string) { - while (removeRegex.test(x)) { - x = x.replace(removeRegex, '') - } - return x -}