mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
committed by
Aiden McClelland
parent
5bcad69cf7
commit
7213d82f1b
@@ -11,7 +11,6 @@ const ICONS = [
|
|||||||
'arrow-back',
|
'arrow-back',
|
||||||
'arrow-forward',
|
'arrow-forward',
|
||||||
'arrow-up',
|
'arrow-up',
|
||||||
'briefcase-outline',
|
|
||||||
'brush-outline',
|
'brush-outline',
|
||||||
'bookmark-outline',
|
'bookmark-outline',
|
||||||
'cellular-outline',
|
'cellular-outline',
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import { NgModule } from '@angular/core'
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
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 { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||||
import { MaskPipeModule } from 'src/app/pipes/mask/mask.module'
|
import { MaskPipeModule } from 'src/app/pipes/mask/mask.module'
|
||||||
import {
|
import {
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
TextSpinnerComponentModule,
|
TextSpinnerComponentModule,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
|
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: AppPropertiesPage,
|
component: AppCredentialsPage,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -26,7 +27,8 @@ const routes: Routes = [
|
|||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
TextSpinnerComponentModule,
|
TextSpinnerComponentModule,
|
||||||
MaskPipeModule,
|
MaskPipeModule,
|
||||||
|
SkeletonListComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [AppPropertiesPage],
|
declarations: [AppCredentialsPage],
|
||||||
})
|
})
|
||||||
export class AppPropertiesPageModule {}
|
export class AppCredentialsPageModule {}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>Credentials</ion-title>
|
||||||
|
<ion-buttons *ngIf="!loading" slot="end">
|
||||||
|
<ion-button (click)="refresh()">
|
||||||
|
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||||
|
Refresh
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content class="ion-padding-top with-widgets">
|
||||||
|
<!-- loading -->
|
||||||
|
<skeleton-list *ngIf="loading; else loaded"></skeleton-list>
|
||||||
|
|
||||||
|
<!-- loaded -->
|
||||||
|
<ng-template #loaded>
|
||||||
|
<!-- no credentials -->
|
||||||
|
<ion-item *ngIf="credentials | empty else hasCredentials">
|
||||||
|
<ion-label>
|
||||||
|
<p>No credentials</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ng-template #hasCredentials>
|
||||||
|
<ion-item-group>
|
||||||
|
<ion-item *ngFor="let cred of credentials | keyvalue: asIsOrder">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ cred.key }}</h2>
|
||||||
|
<p class="courier-new">
|
||||||
|
{{ unmasked[cred.key] ? cred.value : (cred.value | mask : 64) }}
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
<div slot="end">
|
||||||
|
<ion-button fill="clear" (click)="toggleMask(cred.key)">
|
||||||
|
<ion-icon
|
||||||
|
slot="icon-only"
|
||||||
|
[name]="unmasked[cred.key] ? 'eye-off-outline' : 'eye-outline'"
|
||||||
|
size="small"
|
||||||
|
></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button fill="clear" (click)="copy(cred.value)">
|
||||||
|
<ion-icon
|
||||||
|
slot="icon-only"
|
||||||
|
name="copy-outline"
|
||||||
|
size="small"
|
||||||
|
></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
</ion-item-group>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</ion-content>
|
||||||
@@ -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<string, string> = {}
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>Properties</ion-title>
|
|
||||||
<ion-buttons *ngIf="!loading" slot="end">
|
|
||||||
<ion-button (click)="refresh()">
|
|
||||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
|
||||||
Refresh
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding-top with-widgets">
|
|
||||||
<text-spinner
|
|
||||||
*ngIf="loading; else loaded"
|
|
||||||
text="Loading Properties"
|
|
||||||
></text-spinner>
|
|
||||||
|
|
||||||
<ng-template #loaded>
|
|
||||||
<!-- not running -->
|
|
||||||
<ion-item *ngIf="stopped$ | async" class="ion-margin-bottom">
|
|
||||||
<ion-label>
|
|
||||||
<p>
|
|
||||||
<ion-text color="warning">
|
|
||||||
Service is stopped. Information on this page could be inaccurate.
|
|
||||||
</ion-text>
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- no properties -->
|
|
||||||
<ion-item *ngIf="properties | empty">
|
|
||||||
<ion-label>
|
|
||||||
<p>No properties.</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- properties -->
|
|
||||||
<ion-item-group *ngIf="!(properties | empty)">
|
|
||||||
<div *ngFor="let prop of node | keyvalue: asIsOrder">
|
|
||||||
<!-- object -->
|
|
||||||
<ion-item
|
|
||||||
button
|
|
||||||
detail="true"
|
|
||||||
*ngIf="prop.value.type === 'object'"
|
|
||||||
(click)="goToNested(prop.key)"
|
|
||||||
>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="prop.value.description"
|
|
||||||
fill="clear"
|
|
||||||
slot="start"
|
|
||||||
(click)="presentDescription(prop, $event)"
|
|
||||||
>
|
|
||||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ prop.key }}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<!-- not object -->
|
|
||||||
<ion-item *ngIf="prop.value.type === 'string'">
|
|
||||||
<ion-button
|
|
||||||
*ngIf="prop.value.description"
|
|
||||||
fill="clear"
|
|
||||||
slot="start"
|
|
||||||
(click)="presentDescription(prop, $event)"
|
|
||||||
>
|
|
||||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ prop.key }}</h2>
|
|
||||||
<p class="courier-new">
|
|
||||||
{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value |
|
|
||||||
mask : 64) : prop.value.value }}
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
|
||||||
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
|
|
||||||
<ion-button
|
|
||||||
*ngIf="prop.value.masked"
|
|
||||||
fill="clear"
|
|
||||||
(click)="toggleMask(prop.key)"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
slot="icon-only"
|
|
||||||
[name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'"
|
|
||||||
size="small"
|
|
||||||
></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="prop.value.qr"
|
|
||||||
fill="clear"
|
|
||||||
(click)="showQR(prop.value.value)"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
slot="icon-only"
|
|
||||||
name="qr-code-outline"
|
|
||||||
size="small"
|
|
||||||
></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="prop.value.copyable"
|
|
||||||
fill="clear"
|
|
||||||
(click)="copy(prop.value.value)"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
slot="icon-only"
|
|
||||||
name="copy-outline"
|
|
||||||
size="small"
|
|
||||||
></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
</div>
|
|
||||||
</ion-item-group>
|
|
||||||
</ng-template>
|
|
||||||
</ion-content>
|
|
||||||
@@ -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<DataModel>,
|
|
||||||
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<any> {
|
|
||||||
this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, {
|
|
||||||
queryParams: {
|
|
||||||
pointer: `${this.pointer}/${key}/value`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async copy(text: string): Promise<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -62,16 +62,15 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
description: `Customize ${pkgTitle}`,
|
description: `Customize ${pkgTitle}`,
|
||||||
icon: 'options-outline',
|
icon: 'options-outline',
|
||||||
},
|
},
|
||||||
// properties
|
// credentials
|
||||||
{
|
{
|
||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['properties'], {
|
this.navCtrl.navigateForward(['credentials'], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
}),
|
}),
|
||||||
title: 'Properties',
|
title: 'Credentials',
|
||||||
description:
|
description: 'Password, keys, or other credentials of interest',
|
||||||
'Runtime information, credentials, and other values of interest',
|
icon: 'key-outline',
|
||||||
icon: 'briefcase-outline',
|
|
||||||
},
|
},
|
||||||
// actions
|
// actions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ const routes: Routes = [
|
|||||||
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':pkgId/properties',
|
path: ':pkgId/credentials',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./app-properties/app-properties.module').then(
|
import('./app-credentials/app-credentials.module').then(
|
||||||
m => m.AppPropertiesPageModule,
|
m => m.AppCredentialsPageModule,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Dump, Revision } from 'patch-db-client'
|
import { Dump, Revision } from 'patch-db-client'
|
||||||
import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
|
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 { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
@@ -235,9 +234,8 @@ export module RR {
|
|||||||
|
|
||||||
// package
|
// package
|
||||||
|
|
||||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
export type GetPackageCredentialsReq = { id: string } // package.credentials
|
||||||
export type GetPackagePropertiesRes<T extends number> =
|
export type GetPackageCredentialsRes = Record<string, string>
|
||||||
PackagePropertiesVersioned<T>
|
|
||||||
|
|
||||||
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
|
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
|
||||||
export type GetPackageLogsRes = LogsRes
|
export type GetPackageLogsRes = LogsRes
|
||||||
|
|||||||
@@ -234,9 +234,9 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
// package
|
// package
|
||||||
|
|
||||||
abstract getPackageProperties(
|
abstract getPackageCredentials(
|
||||||
params: RR.GetPackagePropertiesReq,
|
params: RR.GetPackageCredentialsReq,
|
||||||
): Promise<RR.GetPackagePropertiesRes<2>['data']>
|
): Promise<RR.GetPackageCredentialsRes>
|
||||||
|
|
||||||
abstract getPackageLogs(
|
abstract getPackageLogs(
|
||||||
params: RR.GetPackageLogsReq,
|
params: RR.GetPackageLogsReq,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { ApiService } from './embassy-api.service'
|
import { ApiService } from './embassy-api.service'
|
||||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
|
||||||
import { ConfigService } from '../config.service'
|
import { ConfigService } from '../config.service'
|
||||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
@@ -405,12 +404,10 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
// package
|
// package
|
||||||
|
|
||||||
async getPackageProperties(
|
async getPackageCredentials(
|
||||||
params: RR.GetPackagePropertiesReq,
|
params: RR.GetPackageCredentialsReq,
|
||||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
): Promise<RR.GetPackageCredentialsRes> {
|
||||||
return this.rpcRequest({ method: 'package.properties', params }).then(
|
return this.rpcRequest({ method: 'package.credentials', params })
|
||||||
parsePropertiesPermissive,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPackageLogs(
|
async getPackageLogs(
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
PackageState,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
|
||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||||
import {
|
import {
|
||||||
@@ -667,12 +666,13 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
// package
|
// package
|
||||||
|
|
||||||
async getPackageProperties(
|
async getPackageCredentials(
|
||||||
params: RR.GetPackagePropertiesReq,
|
params: RR.GetPackageCredentialsReq,
|
||||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
): Promise<RR.GetPackageCredentialsRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return '' as any
|
return {
|
||||||
// return parsePropertiesPermissive(Mock.PackageProperties)
|
password: 'specialPassword$',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPackageLogs(
|
async getPackageLogs(
|
||||||
|
|||||||
@@ -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 number> = T extends 1
|
|
||||||
? PackagePropertiesV1
|
|
||||||
: T extends 2
|
|
||||||
? PackagePropertiesV2
|
|
||||||
: never
|
|
||||||
|
|
||||||
type PackagePropertyString = typeof matchPackagePropertyString._TYPE
|
|
||||||
|
|
||||||
export type PackagePropertiesVersioned<T extends number> = {
|
|
||||||
version: T
|
|
||||||
data: PackagePropertiesVersionedData<T>
|
|
||||||
}
|
|
||||||
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<PackagePropertiesV2>()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user