diff --git a/frontend/projects/diagnostic-ui/src/app/services/http.service.ts b/frontend/projects/diagnostic-ui/src/app/services/http.service.ts index 8e814e73f..5bcfafe94 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/http.service.ts +++ b/frontend/projects/diagnostic-ui/src/app/services/http.service.ts @@ -5,40 +5,43 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http' providedIn: 'root', }) export class HttpService { + constructor(private readonly http: HttpClient) {} - constructor ( - private readonly http: HttpClient, - ) { } - - async rpcRequest (options: RPCOptions): Promise { + async rpcRequest(options: RPCOptions): Promise { const res = await this.httpRequest>(options) if (isRpcError(res)) throw new RpcError(res.error) if (isRpcSuccess(res)) return res.result } - async httpRequest (body: RPCOptions): Promise { + async httpRequest(body: RPCOptions): Promise { const url = `${window.location.protocol}//${window.location.hostname}:${window.location.port}/rpc/v1` - return this.http.post(url, body) - .toPromise().then(a => a as T) - .catch(e => { throw new HttpError(e) }) + return this.http + .post(url, body) + .toPromise() + .then(a => a as T) + .catch(e => { + throw new HttpError(e) + }) } } -function RpcError (e: RPCError['error']): void { +function RpcError(e: RPCError['error']): void { const { code, message, data } = e this.code = code - this.message = message if (typeof data === 'string') { - this.details = e.data - this.revision = null + this.message = `${message}\n\n${data}` } else { - this.details = data.details + if (data.details) { + this.message = `${message}\n\n${data.details}` + } else { + this.message = message + } } } -function HttpError (e: HttpErrorResponse): void { +function HttpError(e: HttpErrorResponse): void { const { status, statusText } = e this.code = status @@ -47,11 +50,15 @@ function HttpError (e: HttpErrorResponse): void { this.revision = null } -function isRpcError (arg: { error: Error } | { result: Result}): arg is { error: Error } { +function isRpcError( + arg: { error: Error } | { result: Result }, +): arg is { error: Error } { return !!(arg as any).error } -function isRpcSuccess (arg: { error: Error } | { result: Result}): arg is { result: Result } { +function isRpcSuccess( + arg: { error: Error } | { result: Result }, +): arg is { result: Result } { return !!(arg as any).result } @@ -84,14 +91,18 @@ export interface RPCSuccess extends RPCBase { export interface RPCError extends RPCBase { error: { - code: number, + code: number message: string - data?: { - details: string - } | string + data?: + | { + details: string + } + | string } } export type RPCResponse = RPCSuccess | RPCError -type HttpError = HttpErrorResponse & { error: { code: string, message: string } } +type HttpError = HttpErrorResponse & { + error: { code: string; message: string } +} diff --git a/frontend/projects/setup-wizard/src/app/app.component.ts b/frontend/projects/setup-wizard/src/app/app.component.ts index fcbbccc5c..071c4788c 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.ts +++ b/frontend/projects/setup-wizard/src/app/app.component.ts @@ -10,14 +10,14 @@ import { StateService } from './services/state.service' styleUrls: ['app.component.scss'], }) export class AppComponent { - constructor ( + constructor( private readonly apiService: ApiService, private readonly errorToastService: ErrorToastService, private readonly navCtrl: NavController, private readonly stateService: StateService, - ) { } + ) {} - async ngOnInit () { + async ngOnInit() { try { const status = await this.apiService.getStatus() if (status.migrating || status['product-key']) { @@ -30,7 +30,7 @@ export class AppComponent { await this.navCtrl.navigateForward(`/recover`) } } catch (e) { - this.errorToastService.present(`${e.message}: ${e.details}`) + this.errorToastService.present(e.message) } } } diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index b906f05ca..972c78e7d 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -1,6 +1,15 @@ import { Component } from '@angular/core' -import { AlertController, LoadingController, ModalController, NavController } from '@ionic/angular' -import { ApiService, DiskInfo, DiskRecoverySource } from 'src/app/services/api/api.service' +import { + AlertController, + LoadingController, + ModalController, + NavController, +} from '@ionic/angular' +import { + ApiService, + DiskInfo, + DiskRecoverySource, +} from 'src/app/services/api/api.service' import { ErrorToastService } from 'src/app/services/error-toast.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -14,7 +23,7 @@ export class EmbassyPage { storageDrives: DiskInfo[] = [] loading = true - constructor ( + constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, private readonly modalController: ModalController, @@ -22,26 +31,34 @@ export class EmbassyPage { private readonly stateService: StateService, private readonly loadingCtrl: LoadingController, private readonly errorToastService: ErrorToastService, - ) { } + ) {} - async ngOnInit () { + async ngOnInit() { await this.getDrives() } - tooSmall (drive: DiskInfo) { + tooSmall(drive: DiskInfo) { return drive.capacity < 34359738368 } - async refresh () { + async refresh() { this.loading = true await this.getDrives() } - async getDrives () { + async getDrives() { this.loading = true try { const { disks, reconnect } = await this.apiService.getDrives() - this.storageDrives = disks.filter(d => !d.partitions.map(p => p.logicalname).includes((this.stateService.recoverySource as DiskRecoverySource)?.logicalname)) + this.storageDrives = disks.filter( + d => + !d.partitions + .map(p => p.logicalname) + .includes( + (this.stateService.recoverySource as DiskRecoverySource) + ?.logicalname, + ), + ) if (!this.storageDrives.length && reconnect.length) { const list = `
    ${reconnect.map(recon => `
  • ${recon}
  • `)}
` const alert = await this.alertCtrl.create({ @@ -63,7 +80,7 @@ export class EmbassyPage { } } - async chooseDrive (drive: DiskInfo) { + async chooseDrive(drive: DiskInfo) { if (!!drive.partitions.find(p => p.used) || !!drive.guid) { const alert = await this.alertCtrl.create({ header: 'Warning', @@ -96,7 +113,7 @@ export class EmbassyPage { } } - private async presentModalPassword (drive: DiskInfo): Promise { + private async presentModalPassword(drive: DiskInfo): Promise { const modal = await this.modalController.create({ component: PasswordPage, componentProps: { @@ -110,7 +127,7 @@ export class EmbassyPage { await modal.present() } - private async setupEmbassy (drive: DiskInfo, password: string): Promise { + private async setupEmbassy(drive: DiskInfo, password: string): Promise { const loader = await this.loadingCtrl.create({ message: 'Transferring encrypted data. This could take a while...', }) @@ -125,7 +142,9 @@ export class EmbassyPage { await this.navCtrl.navigateForward(`/init`) } } catch (e) { - this.errorToastService.present(`${e.message}: ${e.details}. Restart Embassy to try again.`) + this.errorToastService.present( + `${e.message}\n\nRestart Embassy to try again.`, + ) console.error(e) } finally { loader.dismiss() diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts index 659ba94a0..cc429265c 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -1,5 +1,11 @@ import { Component, Input } from '@angular/core' -import { AlertController, IonicSafeString, LoadingController, ModalController, NavController } from '@ionic/angular' +import { + AlertController, + IonicSafeString, + LoadingController, + ModalController, + NavController, +} from '@ionic/angular' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' import { ErrorToastService } from 'src/app/services/error-toast.service' @@ -17,7 +23,7 @@ export class RecoverPage { mappedDrives: MappedDisk[] = [] hasShownGuidAlert = false - constructor ( + constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, @@ -26,45 +32,48 @@ export class RecoverPage { private readonly loadingCtrl: LoadingController, private readonly errorToastService: ErrorToastService, public readonly stateService: StateService, - ) { } + ) {} - async ngOnInit () { + async ngOnInit() { await this.getDrives() } - async refresh () { + async refresh() { this.loading = true await this.getDrives() } - driveClickable (mapped: MappedDisk) { - return mapped.drive['embassy-os']?.full && (this.stateService.hasProductKey || mapped.is02x) + driveClickable(mapped: MappedDisk) { + return ( + mapped.drive['embassy-os']?.full && + (this.stateService.hasProductKey || mapped.is02x) + ) } - async getDrives () { + async getDrives() { this.mappedDrives = [] try { const { disks, reconnect } = await this.apiService.getDrives() - disks.filter(d => d.partitions.length).forEach(d => { - d.partitions.forEach(p => { - const drive: DiskBackupTarget = { - vendor: d.vendor, - model: d.model, - logicalname: p.logicalname, - label: p.label, - capacity: p.capacity, - used: p.used, - 'embassy-os': p['embassy-os'], - } - this.mappedDrives.push( - { + disks + .filter(d => d.partitions.length) + .forEach(d => { + d.partitions.forEach(p => { + const drive: DiskBackupTarget = { + vendor: d.vendor, + model: d.model, + logicalname: p.logicalname, + label: p.label, + capacity: p.capacity, + used: p.used, + 'embassy-os': p['embassy-os'], + } + this.mappedDrives.push({ hasValidBackup: p['embassy-os']?.full, is02x: drive['embassy-os']?.version.startsWith('0.2'), drive, - }, - ) + }) + }) }) - }) if (!this.mappedDrives.length && reconnect.length) { const list = `
    ${reconnect.map(recon => `
  • ${recon}
  • `)}
` @@ -85,7 +94,11 @@ export class RecoverPage { if (!!importableDrive && !this.hasShownGuidAlert) { const alert = await this.alertCtrl.create({ header: 'Embassy Data Drive Detected', - message: new IonicSafeString(`${importableDrive.vendor || 'Unknown Vendor'} - ${importableDrive.model || 'Unknown Model' } contains Embassy data. To use this drive and its data as-is, click "Use Drive". This will complete the setup process.

Important. If you are trying to restore from backup or update from 0.2.x, DO NOT click "Use Drive". Instead, click "Cancel" and follow instructions.`), + message: new IonicSafeString( + `${importableDrive.vendor || 'Unknown Vendor'} - ${ + importableDrive.model || 'Unknown Model' + } contains Embassy data. To use this drive and its data as-is, click "Use Drive". This will complete the setup process.

Important. If you are trying to restore from backup or update from 0.2.x, DO NOT click "Use Drive". Instead, click "Cancel" and follow instructions.`, + ), buttons: [ { role: 'cancel', @@ -103,13 +116,13 @@ export class RecoverPage { this.hasShownGuidAlert = true } } catch (e) { - this.errorToastService.present(`${e.message}: ${e.details}`) + this.errorToastService.present(e.message) } finally { this.loading = false } } - async presentModalCifs (): Promise { + async presentModalCifs(): Promise { const modal = await this.modalCtrl.create({ component: CifsModal, }) @@ -130,7 +143,7 @@ export class RecoverPage { await modal.present() } - async select (target: DiskBackupTarget) { + async select(target: DiskBackupTarget) { const is02x = target['embassy-os'].version.startsWith('0.2') if (this.stateService.hasProductKey) { @@ -154,7 +167,8 @@ export class RecoverPage { if (!is02x) { const alert = await this.alertCtrl.create({ header: 'Error', - message: 'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.', + message: + 'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.', buttons: [ { role: 'cancel', @@ -179,7 +193,7 @@ export class RecoverPage { } } - private async importDrive (guid: string) { + private async importDrive(guid: string) { const loader = await this.loadingCtrl.create({ message: 'Importing Drive', }) @@ -188,13 +202,13 @@ export class RecoverPage { await this.stateService.importDrive(guid) await this.navCtrl.navigateForward(`/init`) } catch (e) { - this.errorToastService.present(`${e.message}: ${e.details}`) + this.errorToastService.present(e.message) } finally { loader.dismiss() } } - private async selectRecoverySource (logicalname: string, password?: string) { + private async selectRecoverySource(logicalname: string, password?: string) { this.stateService.recoverySource = { type: 'disk', logicalname, @@ -204,7 +218,6 @@ export class RecoverPage { } } - @Component({ selector: 'drive-status', templateUrl: './drive-status.component.html', @@ -215,7 +228,6 @@ export class DriveStatusComponent { @Input() is02x: boolean } - interface MappedDisk { is02x: boolean hasValidBackup: boolean diff --git a/frontend/projects/setup-wizard/src/app/services/api/http.service.ts b/frontend/projects/setup-wizard/src/app/services/api/http.service.ts index 35a15ac18..ee8e94bc2 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/http.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/http.service.ts @@ -1,5 +1,10 @@ import { Injectable } from '@angular/core' -import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http' +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpParams, +} from '@angular/common/http' import { Observable } from 'rxjs' import * as aesjs from 'aes-js' import * as pbkdf2 from 'pbkdf2' @@ -11,15 +16,12 @@ export class HttpService { fullUrl: string productKey: string - constructor ( - private readonly http: HttpClient, - ) { + constructor(private readonly http: HttpClient) { const port = window.location.port this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}/rpc/v1` } - async rpcRequest (body: RPCOptions, encrypted = true): Promise { - + async rpcRequest(body: RPCOptions, encrypted = true): Promise { const httpOpts = { method: Method.POST, body, @@ -42,17 +44,17 @@ export class HttpService { if (isRpcSuccess(res)) return res.result } - async encryptedHttpRequest (httpOpts: { - body: RPCOptions; - url: string; + async encryptedHttpRequest(httpOpts: { + body: RPCOptions + url: string }): Promise { - const urlIsRelative = httpOpts.url.startsWith('/') - const url = urlIsRelative ? - this.fullUrl + httpOpts.url : - httpOpts.url + const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url - const encryptedBody = await AES_CTR.encryptPbkdf2(this.productKey, encodeUtf8(JSON.stringify(httpOpts.body))) + const encryptedBody = await AES_CTR.encryptPbkdf2( + this.productKey, + encodeUtf8(JSON.stringify(httpOpts.body)), + ) const options = { responseType: 'arraybuffer', body: encryptedBody.buffer, @@ -67,9 +69,14 @@ export class HttpService { const req = this.http.post(url, options.body, options) - return (req) + return req .toPromise() - .then(res => AES_CTR.decryptPbkdf2(this.productKey, (res as any).body as ArrayBuffer)) + .then(res => + AES_CTR.decryptPbkdf2( + this.productKey, + (res as any).body as ArrayBuffer, + ), + ) .then(res => JSON.parse(res)) .catch(e => { if (!e.status && !e.statusText) { @@ -80,46 +87,56 @@ export class HttpService { }) } - async httpRequest (httpOpts: { - body: RPCOptions; - url: string; + async httpRequest(httpOpts: { + body: RPCOptions + url: string }): Promise { const urlIsRelative = httpOpts.url.startsWith('/') - const url = urlIsRelative ? - this.fullUrl + httpOpts.url : - httpOpts.url + const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url const options = { responseType: 'json', body: httpOpts.body, observe: 'events', reportProgress: false, - headers: { 'content-type': 'application/json', accept: 'application/json' }, + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, } as any - const req: Observable<{ body: T }> = this.http.post(url, httpOpts.body, options) as any + const req: Observable<{ body: T }> = this.http.post( + url, + httpOpts.body, + options, + ) as any - return (req) + return req .toPromise() .then(res => res.body) - .catch(e => { throw new HttpError(e) }) + .catch(e => { + throw new HttpError(e) + }) } } -function RpcError (e: RPCError['error']): void { +function RpcError(e: RPCError['error']): void { const { code, message, data } = e this.code = code - this.message = message - if (typeof data !== 'string') { - this.details = data.details + if (typeof data === 'string') { + this.message = `${message}\n\n${data}` } else { - this.details = data + if (data.details) { + this.message = `${message}\n\n${data.details}` + } else { + this.message = message + } } } -function HttpError (e: HttpErrorResponse): void { +function HttpError(e: HttpErrorResponse): void { const { status, statusText } = e this.code = status @@ -127,17 +144,21 @@ function HttpError (e: HttpErrorResponse): void { this.details = null } -function EncryptionError (e: HttpErrorResponse): void { +function EncryptionError(e: HttpErrorResponse): void { this.code = null this.message = 'Invalid Key' this.details = null } -function isRpcError (arg: { error: Error } | { result: Result }): arg is { error: Error } { +function isRpcError( + arg: { error: Error } | { result: Result }, +): arg is { error: Error } { return !!(arg as any).error } -function isRpcSuccess (arg: { error: Error } | { result: Result }): arg is { result: Result } { +function isRpcSuccess( + arg: { error: Error } | { result: Result }, +): arg is { result: Result } { return !!(arg as any).result } @@ -152,7 +173,7 @@ export enum Method { export interface RPCOptions { method: string params?: { - [param: string]: string | number | boolean | object | string[] | number[]; + [param: string]: string | number | boolean | object | string[] | number[] } } @@ -172,27 +193,35 @@ export interface RPCSuccess extends RPCBase { export interface RPCError extends RPCBase { error: { - code: number, + code: number message: string - data?: { - details: string - } | string + data?: + | { + details: string + } + | string } } export type RPCResponse = RPCSuccess | RPCError -type HttpError = HttpErrorResponse & { error: { code: string, message: string } } +type HttpError = HttpErrorResponse & { + error: { code: string; message: string } +} export interface HttpOptions { method: Method url: string - headers?: HttpHeaders | { - [header: string]: string | string[] - } - params?: HttpParams | { - [param: string]: string | string[] - } + headers?: + | HttpHeaders + | { + [header: string]: string | string[] + } + params?: + | HttpParams + | { + [param: string]: string | string[] + } responseType?: 'json' | 'text' | 'arrayBuffer' withCredentials?: boolean body?: any @@ -200,7 +229,10 @@ export interface HttpOptions { } type AES_CTR = { - encryptPbkdf2: (secretKey: string, messageBuffer: Uint8Array) => Promise + encryptPbkdf2: ( + secretKey: string, + messageBuffer: Uint8Array, + ) => Promise decryptPbkdf2: (secretKey, arr: ArrayBuffer) => Promise } @@ -211,7 +243,10 @@ export const AES_CTR: AES_CTR = { const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') - const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter)) + const aesCtr = new aesjs.ModeOfOperation.ctr( + key, + new aesjs.Counter(counter), + ) const encryptedBytes = aesCtr.encrypt(messageBuffer) return new Uint8Array([...counter, ...salt, ...encryptedBytes]) }, @@ -223,21 +258,26 @@ export const AES_CTR: AES_CTR = { const cipher = buff.slice(32) const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256') - const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter)) + const aesCtr = new aesjs.ModeOfOperation.ctr( + key, + new aesjs.Counter(counter), + ) const decryptedBytes = aesCtr.decrypt(cipher) return aesjs.utils.utf8.fromBytes(decryptedBytes) }, } -export const encode16 = (buffer: Uint8Array) => buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') -export const decode16 = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))) +export const encode16 = (buffer: Uint8Array) => + buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '') +export const decode16 = hexString => + new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))) -export function encodeUtf8 (str: string): Uint8Array { +export function encodeUtf8(str: string): Uint8Array { const encoder = new TextEncoder() return encoder.encode(str) } -export function decodeUtf8 (arr: Uint8Array): string { +export function decodeUtf8(arr: Uint8Array): string { return new TextDecoder().decode(arr) -} \ No newline at end of file +} diff --git a/frontend/projects/setup-wizard/src/app/services/state.service.ts b/frontend/projects/setup-wizard/src/app/services/state.service.ts index dcfa99917..df59e5edd 100644 --- a/frontend/projects/setup-wizard/src/app/services/state.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/state.service.ts @@ -52,7 +52,7 @@ export class StateService { progress = await this.apiService.getRecoveryStatus() } catch (e) { this.errorToastService.present( - `${e.message}: ${e.details}.\nRestart Embassy to try again.`, + `${e.message}\n\nRestart Embassy to try again.`, ) } if (progress) { diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index 270003002..59bb22384 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -133,7 +133,7 @@ - + @@ -164,6 +164,7 @@ + diff --git a/frontend/projects/ui/src/app/components/form-object/form-object.component.html b/frontend/projects/ui/src/app/components/form-object/form-object.component.html index c39ef05dd..c78c9dc87 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-object.component.html +++ b/frontend/projects/ui/src/app/components/form-object/form-object.component.html @@ -1,23 +1,40 @@ -
+

{{ unionSpec.tag.name }}

- + {{ unionSpec.tag.name }} - + {{ unionSpec.tag['variant-names'][option] }} @@ -25,70 +42,124 @@
- +

- +

- - + - - - + + + + + + + + - {{ spec.units }} + {{ spec.units }} {{ spec.name }} - + {{ spec.name }} - + {{ spec['value-names'][option] }}
- + - - + @@ -96,17 +167,18 @@ [id]="getElementId(entry.key)" [ngStyle]="{ 'max-height': objectDisplay[entry.key].height, - 'overflow': 'hidden', + overflow: 'hidden', 'transition-property': 'max-height', 'transition-duration': '.25s' }" - > + >
- + - - + + Add @@ -133,25 +215,38 @@
- + - + @@ -160,25 +255,41 @@ [id]="getElementId(entry.key, i)" [ngStyle]="{ 'max-height': objectListDisplay[entry.key][i].height, - 'overflow': 'hidden', + overflow: 'hidden', 'transition-property': 'max-height', 'transition-duration': '.5s', 'transition-delay': '.05s' }" - > + > -
- +
+ Delete @@ -186,16 +297,24 @@
- + - + @@ -212,18 +331,28 @@ - +

- +

- - + +

{{ getEnumListDisplay(formArr.value, $any(spec.spec)) }}

@@ -240,4 +369,4 @@
- \ No newline at end of file + diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss index af1932671..e69de29bb 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss @@ -1,7 +0,0 @@ -.centered{ - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - transform: scale(2); -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts index f0cdae299..e642c366d 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts @@ -75,9 +75,7 @@ export class DevConfigPage { value: this.code, }) } catch (e) { - this.errToast.present({ - message: 'Auto save error: Your changes are not saved.', - } as any) + this.errToast.present(e) } finally { this.saving = false } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts index a875bf9ef..be373843a 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts @@ -59,9 +59,7 @@ export class DevInstructionsPage { value: this.code, }) } catch (e) { - this.errToast.present({ - message: 'Auto save error: Your changes are not saved.', - } as any) + this.errToast.present(e) } finally { this.saving = false } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts new file mode 100644 index 000000000..49d738fe8 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { DevManifestPage } from './dev-manifest.page' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' +import { FormsModule } from '@angular/forms' +import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' + +const routes: Routes = [ + { + path: '', + component: DevManifestPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + BadgeMenuComponentModule, + BackupReportPageModule, + FormsModule, + MonacoEditorModule, + ], + declarations: [DevManifestPage], +}) +export class DevManifestPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html new file mode 100644 index 000000000..599afca14 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html @@ -0,0 +1,17 @@ + + + + + + Manifest + + + + + + diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts new file mode 100644 index 000000000..9d0b04d1c --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import * as yaml from 'js-yaml' +import { take } from 'rxjs/operators' +import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' + +@Component({ + selector: 'dev-manifest', + templateUrl: 'dev-manifest.page.html', + styleUrls: ['dev-manifest.page.scss'], +}) +export class DevManifestPage { + projectId: string + editorOptions = { theme: 'vs-dark', language: 'yaml', readOnly: true } + manifest: string = '' + + constructor( + private readonly route: ActivatedRoute, + private readonly patchDb: PatchDbService, + ) {} + + ngOnInit() { + this.projectId = this.route.snapshot.paramMap.get('projectId') + + this.patchDb + .watch$('ui', 'dev', this.projectId) + .pipe(take(1)) + .subscribe(devData => { + this.manifest = yaml.dump(devData['basic-info']) + }) + } +} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html index e580457dc..e266e922f 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html @@ -29,11 +29,9 @@ - - Delete + diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts index 2d034b999..8285b67f7 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts @@ -1,5 +1,7 @@ import { Component } from '@angular/core' import { + ActionSheetButton, + ActionSheetController, AlertController, LoadingController, ModalController, @@ -15,7 +17,6 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' import * as yaml from 'js-yaml' import { v4 } from 'uuid' import { DevData } from 'src/app/services/patch-db/data-model' -import { Subscription } from 'rxjs' import { ErrorToastService } from 'src/app/services/error-toast.service' import { ActivatedRoute } from '@angular/router' import { DestroyService } from '@start9labs/shared' @@ -40,6 +41,7 @@ export class DeveloperListPage { private readonly route: ActivatedRoute, private readonly destroy$: DestroyService, private readonly patch: PatchDbService, + private readonly actionCtrl: ActionSheetController, ) {} ngOnInit() { @@ -75,6 +77,60 @@ export class DeveloperListPage { await modal.present() } + async presentAction(id: string, event: Event) { + event.stopPropagation() + const buttons: ActionSheetButton[] = [ + { + text: 'Edit Name', + icon: 'pencil', + handler: () => { + this.openEditNameModal(id) + }, + }, + { + text: 'Delete', + icon: 'trash', + role: 'destructive', + handler: () => { + this.presentAlertDelete(id) + }, + }, + ] + + const action = await this.actionCtrl.create({ + header: this.devData[id].name, + subHeader: 'Manage project', + mode: 'ios', + buttons, + }) + + await action.present() + } + + async openEditNameModal(id: string) { + const curName = this.devData[id].name + const options: GenericInputOptions = { + title: 'Edit Name', + message: 'Edit the name of your project.', + label: 'Name', + useMask: false, + placeholder: curName, + nullable: true, + initialValue: curName, + buttonText: 'Save', + submitFn: (value: string) => this.editName(id, value), + } + + const modal = await this.modalCtrl.create({ + componentProps: { options }, + cssClass: 'alertlike-modal', + presentingElement: await this.modalCtrl.getTop(), + component: GenericInputComponent, + }) + + await modal.present() + } + async createProject(name: string) { // fail silently if duplicate project name if ( @@ -104,14 +160,13 @@ export class DeveloperListPage { await this.api.setDbValue({ pointer: `/dev`, value: { [id]: def } }) } } catch (e) { - this.errToast.present({ message: `Error saving project data` } as any) + this.errToast.present(e) } finally { loader.dismiss() } } - async presentAlertDelete(id: string, event: Event) { - event.stopPropagation() + async presentAlertDelete(id: string) { const alert = await this.alertCtrl.create({ header: 'Caution', message: `Are you sure you want to delete this project?`, @@ -132,6 +187,23 @@ export class DeveloperListPage { await alert.present() } + async editName(id: string, newName: string) { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Saving...', + cssClass: 'loader', + }) + await loader.present() + + try { + await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName }) + } catch (e) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + async delete(id: string) { const loader = await this.loadingCtrl.create({ spinner: 'lines', @@ -145,7 +217,7 @@ export class DeveloperListPage { delete devDataToSave[id] await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave }) } catch (e) { - this.errToast.present({ message: `Error deleting project data` } as any) + this.errToast.present(e) } finally { loader.dismiss() } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts index dd641d94b..998e1a52c 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts @@ -5,6 +5,10 @@ import { RouterModule, Routes } from '@angular/router' import { DeveloperMenuPage } from './developer-menu.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' +import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' +import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' +import { FormsModule } from '@angular/forms' +import { SharedPipesModule } from '../../../../../../shared/src/pipes/shared/shared.module' const routes: Routes = [ { @@ -20,6 +24,10 @@ const routes: Routes = [ RouterModule.forChild(routes), BadgeMenuComponentModule, BackupReportPageModule, + GenericFormPageModule, + FormsModule, + MonacoEditorModule, + SharedPipesModule, ], declarations: [DeveloperMenuPage], }) diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html index f0a9c68d5..dfbe35ad1 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html @@ -4,11 +4,32 @@ {{ patchDb.data.ui.dev[projectId].name}} + + View Manifest + - Components + + + +

Basic Info

+

Complete basic info for your package

+
+ + +
diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts index 6c7adba4e..92d206981 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts @@ -1,20 +1,90 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import { LoadingController, ModalController } from '@ionic/angular' +import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' +import { BasicInfo, getBasicInfoSpec } from './form-info' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ErrorToastService } from 'src/app/services/error-toast.service' +import { takeUntil } from 'rxjs/operators' +import { DevProjectData } from 'src/app/services/patch-db/data-model' +import { DestroyService } from '../../../../../../shared/src/services/destroy.service' +import * as yaml from 'js-yaml' @Component({ selector: 'developer-menu', templateUrl: 'developer-menu.page.html', styleUrls: ['developer-menu.page.scss'], + providers: [DestroyService], }) export class DeveloperMenuPage { projectId: string + projectData: DevProjectData + constructor( private readonly route: ActivatedRoute, + private readonly modalCtrl: ModalController, + private readonly loadingCtrl: LoadingController, + private readonly api: ApiService, + private readonly errToast: ErrorToastService, + private readonly destroy$: DestroyService, public readonly patchDb: PatchDbService, ) {} ngOnInit() { this.projectId = this.route.snapshot.paramMap.get('projectId') + + this.patchDb + .watch$('ui', 'dev', this.projectId) + .pipe(takeUntil(this.destroy$)) + .subscribe(pd => { + this.projectData = pd + }) + } + + async openBasicInfoModal() { + const modal = await this.modalCtrl.create({ + component: GenericFormPage, + componentProps: { + title: 'Basic Info', + spec: getBasicInfoSpec(this.projectData), + buttons: [ + { + text: 'Save', + handler: basicInfo => { + basicInfo.description = { + short: basicInfo.short, + long: basicInfo.long, + } + delete basicInfo.short + delete basicInfo.long + this.saveBasicInfo(basicInfo as BasicInfo) + }, + isSubmit: true, + }, + ], + }, + }) + await modal.present() + } + + async saveBasicInfo(basicInfo: BasicInfo) { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: 'Saving...', + cssClass: 'loader', + }) + await loader.present() + + try { + await this.api.setDbValue({ + pointer: `/dev/${this.projectId}/basic-info`, + value: basicInfo, + }) + } catch (e) { + this.errToast.present(e) + } finally { + loader.dismiss() + } } } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts new file mode 100644 index 000000000..2525a8b07 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts @@ -0,0 +1,164 @@ +import { ConfigSpec } from 'src/app/pkg-config/config-types' +import { DevProjectData } from 'src/app/services/patch-db/data-model' + +export type BasicInfo = { + id: string + title: string + 'service-version-number': string + 'release-notes': string + license: string + 'wrapper-repo': string + 'upstream-repo'?: string + 'support-site'?: string + 'marketing-site'?: string + description: { + short: string + long: string + } +} + +export function getBasicInfoSpec(devData: DevProjectData): ConfigSpec { + const basicInfo = devData['basic-info'] + return { + id: { + type: 'string', + name: 'ID', + description: 'The package identifier used by the OS', + placeholder: 'e.g. bitcoind', + nullable: false, + masked: false, + copyable: true, + pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', + 'pattern-description': 'Must be kebab case', + default: basicInfo?.id, + }, + title: { + type: 'string', + name: 'Title', + description: 'A human readable service title', + placeholder: 'e.g. Bitcoin Core', + nullable: false, + masked: false, + copyable: true, + default: basicInfo ? basicInfo.title : devData.name, + }, + 'service-version-number': { + type: 'string', + name: 'Service Version Number', + description: + 'Service version - accepts up to four digits, where the last confirms to revisions necessary for EmbassyOS - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of the service', + placeholder: 'e.g. 0.1.2.3', + nullable: false, + masked: false, + copyable: true, + pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$', + 'pattern-description': 'Must be valid Emver version', + default: basicInfo?.['service-version-number'], + }, + 'release-notes': { + type: 'string', + name: 'Release Notes', + description: 'A human readable service title', + placeholder: 'e.g. Bitcoin Core', + nullable: false, + masked: false, + copyable: true, + textarea: true, + default: basicInfo?.['release-notes'], + }, + license: { + type: 'enum', + name: 'License', + values: [ + 'gnu-agpl-v3', + 'gnu-gpl-v3', + 'gnu-lgpl-v3', + 'mozilla-public-license-2.0', + 'apache-license-2.0', + 'mit', + 'boost-software-license-1.0', + 'the-unlicense', + 'custom', + ], + 'value-names': { + 'gnu-agpl-v3': 'GNU AGPLv3', + 'gnu-gpl-v3': 'GNU GPLv3', + 'gnu-lgpl-v3': 'GNU LGPLv3', + 'mozilla-public-license-2.0': 'Mozilla Public License 2.0', + 'apache-license-2.0': 'Apache License 2.0', + mit: 'mit', + 'boost-software-license-1.0': 'Boost Software License 1.0', + 'the-unlicense': 'The Unlicense', + custom: 'Custom', + }, + description: 'Example description for enum select', + default: 'mit', + }, + 'wrapper-repo': { + type: 'string', + name: 'Wrapper Repo', + description: + 'The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks', + placeholder: 'e.g. www.github.com/example', + nullable: false, + masked: false, + copyable: true, + default: basicInfo?.['wrapper-repo'], + }, + 'upstream-repo': { + type: 'string', + name: 'Upstream Repo', + description: 'The original project repository URL', + placeholder: 'e.g. www.github.com/example', + nullable: true, + masked: false, + copyable: true, + default: basicInfo?.['upstream-repo'], + }, + 'support-site': { + type: 'string', + name: 'Support Site', + description: 'URL to the support site / channel for the project', + placeholder: 'e.g. www.start9labs.com', + nullable: true, + masked: false, + copyable: true, + default: basicInfo?.['support-site'], + }, + 'marketing-site': { + type: 'string', + name: 'Marketing Site', + description: 'URL to the marketing site / channel for the project', + placeholder: 'e.g. www.start9labs.com', + nullable: true, + masked: false, + copyable: true, + default: basicInfo?.['marketing-site'], + }, + short: { + type: 'string', + name: 'Short Description', + description: + 'This is the first description visible to the user in the marketplace', + nullable: false, + masked: false, + copyable: false, + textarea: true, + default: basicInfo?.description?.short, + pattern: '^.{1,320}$', + 'pattern-description': 'Must be shorter than 320 characters', + }, + long: { + type: 'string', + name: 'Long Description', + description: `This description will display with additional details in the service's individual marketplace page`, + nullable: false, + masked: false, + copyable: false, + textarea: true, + default: basicInfo?.description?.long, + pattern: '^.{1,5000}$', + 'pattern-description': 'Must be shorter than 5000 characters', + }, + } +} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts index d2e34dae0..03720d87d 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts @@ -33,6 +33,13 @@ const routes: Routes = [ m => m.DevInstructionsPageModule, ), }, + { + path: 'projects/:projectId/manifest', + loadChildren: () => + import('./dev-manifest/dev-manifest.module').then( + m => m.DevManifestPageModule, + ), + }, ] @NgModule({ diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index c78c388d1..e3fed3cf0 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -89,6 +89,13 @@ > Update + + Reinstall + { + async present(e: RequestError, link?: string): Promise { console.error(e) if (this.toast) return @@ -36,7 +34,7 @@ export class ErrorToastService { await this.toast.present() } - async dismiss (): Promise { + async dismiss(): Promise { if (this.toast) { await this.toast.dismiss() this.toast = undefined @@ -44,11 +42,11 @@ export class ErrorToastService { } } -export function getErrorMessage (e: RequestError, link?: string): string | IonicSafeString { - let message: string | IonicSafeString - - if (e.message) message = `${message ? message + ' ' : ''}${e.message}` - if (e.details) message = `${message ? message + ': ' : ''}${e.details}` +export function getErrorMessage( + e: RequestError, + link?: string, +): string | IonicSafeString { + let message: string | IonicSafeString = e.message if (!message) { message = 'Unknown Error.' @@ -56,8 +54,10 @@ export function getErrorMessage (e: RequestError, link?: string): string | Ionic } if (link) { - message = new IonicSafeString(`${message}

Get Help`) + message = new IonicSafeString( + `${message}

Get Help`, + ) } return message -} \ No newline at end of file +} diff --git a/frontend/projects/ui/src/app/services/http.service.ts b/frontend/projects/ui/src/app/services/http.service.ts index 1ff2f72d3..89d3721bf 100644 --- a/frontend/projects/ui/src/app/services/http.service.ts +++ b/frontend/projects/ui/src/app/services/http.service.ts @@ -1,5 +1,10 @@ import { Injectable } from '@angular/core' -import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http' +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpParams, +} from '@angular/common/http' import { Observable, from, interval, race } from 'rxjs' import { map, take } from 'rxjs/operators' import { ConfigService } from './config.service' @@ -12,7 +17,7 @@ import { AuthService } from './auth.service' export class HttpService { fullUrl: string - constructor ( + constructor( private readonly http: HttpClient, private readonly config: ConfigService, private readonly auth: AuthService, @@ -21,9 +26,9 @@ export class HttpService { this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}` } - async rpcRequest (rpcOpts: RPCOptions): Promise { + async rpcRequest(rpcOpts: RPCOptions): Promise { const { url, version } = this.config.api - rpcOpts.params = rpcOpts.params || { } + rpcOpts.params = rpcOpts.params || {} const httpOpts: HttpOptions = { method: Method.POST, body: rpcOpts, @@ -40,17 +45,15 @@ export class HttpService { if (isRpcSuccess(res)) return res.result } - async httpRequest (httpOpts: HttpOptions): Promise { + async httpRequest(httpOpts: HttpOptions): Promise { if (httpOpts.withCredentials !== false) { httpOpts.withCredentials = true } const urlIsRelative = httpOpts.url.startsWith('/') - const url = urlIsRelative ? - this.fullUrl + httpOpts.url : - httpOpts.url + const url = urlIsRelative ? this.fullUrl + httpOpts.url : httpOpts.url - Object.keys(httpOpts.params || { }).forEach(key => { + Object.keys(httpOpts.params || {}).forEach(key => { if (httpOpts.params[key] === undefined) { delete httpOpts.params[key] } @@ -69,36 +72,51 @@ export class HttpService { let req: Observable<{ body: T }> switch (httpOpts.method) { - case Method.GET: req = this.http.get(url, options) as any; break - case Method.POST: req = this.http.post(url, httpOpts.body, options) as any; break - case Method.PUT: req = this.http.put(url, httpOpts.body, options) as any; break - case Method.PATCH: req = this.http.patch(url, httpOpts.body, options) as any; break - case Method.DELETE: req = this.http.delete(url, options) as any; break + case Method.GET: + req = this.http.get(url, options) as any + break + case Method.POST: + req = this.http.post(url, httpOpts.body, options) as any + break + case Method.PUT: + req = this.http.put(url, httpOpts.body, options) as any + break + case Method.PATCH: + req = this.http.patch(url, httpOpts.body, options) as any + break + case Method.DELETE: + req = this.http.delete(url, options) as any + break } return (httpOpts.timeout ? withTimeout(req, httpOpts.timeout) : req) .toPromise() .then(res => res.body) - .catch(e => { throw new HttpError(e) }) + .catch(e => { + throw new HttpError(e) + }) } } -function RpcError (e: RPCError['error']): void { +function RpcError(e: RPCError['error']): void { const { code, message, data } = e this.code = code - this.message = message if (typeof data === 'string') { - this.details = e.data + this.message = `${message}\n\n${data}` this.revision = null } else { - this.details = data.details + if (data.details) { + this.message = `${message}\n\n${data.details}` + } else { + this.message = message + } this.revision = data.revision } } -function HttpError (e: HttpErrorResponse): void { +function HttpError(e: HttpErrorResponse): void { const { status, statusText } = e this.code = status @@ -107,11 +125,15 @@ function HttpError (e: HttpErrorResponse): void { this.revision = null } -function isRpcError (arg: { error: Error } | { result: Result}): arg is { error: Error } { +function isRpcError( + arg: { error: Error } | { result: Result }, +): arg is { error: Error } { return !!(arg as any).error } -function isRpcSuccess (arg: { error: Error } | { result: Result}): arg is { result: Result } { +function isRpcSuccess( + arg: { error: Error } | { result: Result }, +): arg is { result: Result } { return !!(arg as any).result } @@ -154,36 +176,49 @@ export interface RPCError extends RPCBase { error: { code: number message: string - data?: { - details: string - revision: Revision | null - debug: string | null - } | string + data?: + | { + details: string + revision: Revision | null + debug: string | null + } + | string } } export type RPCResponse = RPCSuccess | RPCError -type HttpError = HttpErrorResponse & { error: { code: string, message: string } } +type HttpError = HttpErrorResponse & { + error: { code: string; message: string } +} export interface HttpOptions { method: Method url: string - headers?: HttpHeaders | { - [header: string]: string | string[] - } - params?: HttpParams | { - [param: string]: string | string[] - } + headers?: + | HttpHeaders + | { + [header: string]: string | string[] + } + params?: + | HttpParams + | { + [param: string]: string | string[] + } responseType?: 'json' | 'text' withCredentials?: boolean body?: any timeout?: number } -function withTimeout (req: Observable, timeout: number): Observable { +function withTimeout(req: Observable, timeout: number): Observable { return race( from(req.toPromise()), // this guarantees it only emits on completion, intermediary emissions are suppressed. - interval(timeout).pipe(take(1), map(() => { throw new Error('timeout') })), + interval(timeout).pipe( + take(1), + map(() => { + throw new Error('timeout') + }), + ), ) } 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 9654bcecf..dfefe3aa2 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 @@ -1,5 +1,6 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' import { InstallProgress, PackageState } from '@start9labs/shared' +import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' export interface DataModel { 'server-info': ServerInfo @@ -28,11 +29,14 @@ export interface UIMarketplaceData { } export interface DevData { - [id: string]: { - name: string - instructions?: string - config?: string - } + [id: string]: DevProjectData +} + +export interface DevProjectData { + name: string + instructions?: string + config?: string + 'basic-info'?: BasicInfo } export interface ServerInfo {