diff --git a/patch-db b/patch-db index 16ba75225..87fbca556 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 16ba75225b2470a1b28b0da64578a43e8ad522f7 +Subproject commit 87fbca556c4097cb1cc455b1a07e9b7238378aef diff --git a/ui/README.md b/ui/README.md index 979c820ef..1923c389b 100644 --- a/ui/README.md +++ b/ui/README.md @@ -12,20 +12,8 @@ npm --version v8.0.0 ``` -### Running The Mock Development Server - -`git clone https://github.com/Start9Labs/ws-example.git` - -`cd ws-example` - -`git submodule update --init --recursive` - -`cargo run -- -vvv -c example-config.toml` - ### Building Embassy UI -**In a new terminal window, from `embassy-os/ui` run:** - `git clone https://github.com/Start9Labs/embassy-os.git` `cd embassy-os` @@ -46,30 +34,13 @@ In `config.json`, edit the "mocks" section to look like the following: ``` "mocks": { "enabled": true, - "connection": "ws", - "rpcPort": "5959", - "wsPort": "5960", "maskAs": "tor", "skipStartupAlerts": true } ``` -Valid values for "connection" are `ws` and `poll`. - Valid values for "maskAs" are `tor` and `lan`. -You can also enable or disable startup alerts. - -**Start the client** +**Start the development server** `ionic serve` - -### Updating Server Mocks - -If you want to update mock data inside ws-example, you must do the following: - -1. Stop the ws-example server -1. In es-example, run `rm embassy.db` -1. Delete `patch-db-cache` from your browser's Local Storage -1. Restart ws-example -1. Refresh the browser window diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index be3e7adfa..3712a1eb1 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -51,7 +51,7 @@ import { GlobalErrorHandler } from './services/global-error-handler.service' { provide: ApiService, useFactory: ApiServiceFactory, - deps: [ConfigService, HttpService], + deps: [ConfigService, HttpService, LocalStorageBootstrap], }, { provide: PatchDbService, diff --git a/ui/src/app/components/backup-drives/backup-drives-header.component.html b/ui/src/app/components/backup-drives/backup-drives-header.component.html index 9b81c26ad..1de729022 100644 --- a/ui/src/app/components/backup-drives/backup-drives-header.component.html +++ b/ui/src/app/components/backup-drives/backup-drives-header.component.html @@ -1,7 +1,7 @@ - + {{ title }} diff --git a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index ccbd68be7..ce36ecdf5 100644 --- a/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -1,7 +1,7 @@ - + Actions diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index cc1926c68..dc461b205 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -1,7 +1,7 @@ - + Interfaces diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 5028f5ab8..7b07c02fb 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -20,6 +20,7 @@ export class AppInterfacesPage { @ViewChild(IonContent) content: IonContent ui: LocalInterface | null other: LocalInterface[] = [] + pkgId: string constructor ( private readonly route: ActivatedRoute, @@ -27,8 +28,8 @@ export class AppInterfacesPage { ) { } ngOnInit () { - const pkgId = this.route.snapshot.paramMap.get('pkgId') - const pkg = this.patch.getData()['package-data'][pkgId] + this.pkgId = this.route.snapshot.paramMap.get('pkgId') + const pkg = this.patch.getData()['package-data'][this.pkgId] const interfaces = pkg.manifest.interfaces const uiKey = getUiInterfaceKey(interfaces) diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html index f3086a2ac..cbec4d05c 100644 --- a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html @@ -1,7 +1,7 @@ - + Logs diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts index 9d45d99b3..15883fdeb 100644 --- a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts @@ -16,9 +16,10 @@ export class AppLogsPage { constructor ( private readonly route: ActivatedRoute, private readonly embassyApi: ApiService, - ) { - this.pkgId = this.route.snapshot.paramMap.get('pkgId') + ) { } + ngOnInit () { + this.pkgId = this.route.snapshot.paramMap.get('pkgId') } fetchFetchLogs () { diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html index 202d4f3ae..9d8365124 100644 --- a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html @@ -1,7 +1,7 @@ - + Monitor diff --git a/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html b/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html index c7db4229d..d2907ba15 100644 --- a/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html +++ b/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html @@ -1,7 +1,7 @@ - + Properties diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 946224d1c..fe746c957 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -1,7 +1,7 @@ - + diff --git a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html index 588d77160..c9573eb2e 100644 --- a/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html +++ b/ui/src/app/pages/marketplace-routes/app-release-notes/app-release-notes.page.html @@ -1,7 +1,7 @@ - + Release Notes diff --git a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index 68f3f2566..23ddb3c8e 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -1,7 +1,7 @@ - + Marketplace Listing diff --git a/ui/src/app/pages/server-routes/lan/lan.page.html b/ui/src/app/pages/server-routes/lan/lan.page.html index 21041018b..37d806931 100644 --- a/ui/src/app/pages/server-routes/lan/lan.page.html +++ b/ui/src/app/pages/server-routes/lan/lan.page.html @@ -2,7 +2,7 @@ LAN Settings - + diff --git a/ui/src/app/pages/server-routes/preferences/preferences.page.html b/ui/src/app/pages/server-routes/preferences/preferences.page.html index bab9be1c4..740878f18 100644 --- a/ui/src/app/pages/server-routes/preferences/preferences.page.html +++ b/ui/src/app/pages/server-routes/preferences/preferences.page.html @@ -1,7 +1,7 @@ - + Preferences diff --git a/ui/src/app/pages/server-routes/server-backup/server-backup.page.html b/ui/src/app/pages/server-routes/server-backup/server-backup.page.html index 5f25f0cd2..14bf81aa7 100644 --- a/ui/src/app/pages/server-routes/server-backup/server-backup.page.html +++ b/ui/src/app/pages/server-routes/server-backup/server-backup.page.html @@ -12,7 +12,7 @@ - + Backup Progress diff --git a/ui/src/app/pages/server-routes/server-logs/server-logs.page.html b/ui/src/app/pages/server-routes/server-logs/server-logs.page.html index f3086a2ac..07b9c7217 100644 --- a/ui/src/app/pages/server-routes/server-logs/server-logs.page.html +++ b/ui/src/app/pages/server-routes/server-logs/server-logs.page.html @@ -1,7 +1,7 @@ - + Logs diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html index 9d67c4a9a..80c0ca6cf 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html @@ -1,7 +1,7 @@ - + Monitor diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index 35332dd13..3ff2e1b1f 100644 --- a/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -1,7 +1,7 @@ - + About diff --git a/ui/src/app/pages/server-routes/sessions/sessions.page.html b/ui/src/app/pages/server-routes/sessions/sessions.page.html index 28d885044..b518bb5de 100644 --- a/ui/src/app/pages/server-routes/sessions/sessions.page.html +++ b/ui/src/app/pages/server-routes/sessions/sessions.page.html @@ -1,7 +1,7 @@ - + Active Sessions diff --git a/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html b/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html index 1cc95d4fd..66d17fffa 100644 --- a/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html +++ b/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html @@ -1,7 +1,7 @@ - + SSH Keys diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.html b/ui/src/app/pages/server-routes/wifi/wifi.page.html index 330b1cfda..4be59341a 100644 --- a/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -1,7 +1,7 @@ - + WiFi Settings diff --git a/ui/src/app/services/api/api.service.factory.ts b/ui/src/app/services/api/api.service.factory.ts index 815b50d51..05d678195 100644 --- a/ui/src/app/services/api/api.service.factory.ts +++ b/ui/src/app/services/api/api.service.factory.ts @@ -2,10 +2,11 @@ import { HttpService } from '../http.service' import { MockApiService } from './embassy-mock-api.service' import { LiveApiService } from './embassy-live-api.service' import { ConfigService } from '../config.service' +import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' -export function ApiServiceFactory (config: ConfigService, http: HttpService) { +export function ApiServiceFactory (config: ConfigService, http: HttpService, bootstrapper: LocalStorageBootstrap) { if (config.mocks.enabled) { - return new MockApiService(http) + return new MockApiService(bootstrapper) } else { return new LiveApiService(http) } diff --git a/ui/src/app/services/api/embassy-api.service.ts b/ui/src/app/services/api/embassy-api.service.ts index 8037c7864..bb0cf479b 100644 --- a/ui/src/app/services/api/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy-api.service.ts @@ -5,12 +5,12 @@ import { DataModel } from 'src/app/services/patch-db/data-model' import { RequestError } from '../http.service' export abstract class ApiService implements Source, Http { - protected readonly sync = new Subject>() + protected readonly sync$ = new Subject>() /** PatchDb Source interface. Post/Patch requests provide a source of patches to the db. */ // sequenceStream '_' is not used by the live api, but is overridden by the mock watch$ (_?: Store): Observable> { - return this.sync.asObservable() + return this.sync$.asObservable() } connectionMade$ = new Subject() @@ -201,12 +201,12 @@ export abstract class ApiService implements Source, Http { return f(a) .catch((e: RequestError) => { - if (e.revision) this.sync.next(e.revision) + if (e.revision) this.sync$.next(e.revision) throw e }) .then(({ response, revision }) => { this.connectionMade$.next() - if (revision) this.sync.next(revision) + if (revision) this.sync$.next(revision) return response }) } diff --git a/ui/src/app/services/api/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy-mock-api.service.ts index bf3ef8f85..1850417b4 100644 --- a/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1,22 +1,27 @@ import { Injectable } from '@angular/core' import { pauseFor } from '../../util/misc.util' import { ApiService } from './embassy-api.service' -import { PatchOp } from 'patch-db-client' -import { DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model' +import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client' +import { DataModel, DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model' import { Log, RR, WithRevision } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' -import { HttpService } from '../http.service' import markdown from 'raw-loader!src/assets/markdown/md-sample.md' -import { Operation } from 'fast-json-patch' +import { BehaviorSubject } from 'rxjs' +import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' +import { mockPatchData } from './mock-patch' @Injectable() export class MockApiService extends ApiService { + readonly mockPatch$ = new BehaviorSubject>(undefined) private readonly revertTime = 4000 + sequence: number constructor ( - private readonly http: HttpService, - ) { super() } + private readonly bootstrapper: LocalStorageBootstrap, + ) { + super() + } async getStatic (url: string): Promise { await pauseFor(2000) @@ -26,22 +31,35 @@ export class MockApiService extends ApiService { // db async getRevisions (since: number): Promise { - return this.http.rpcRequest({ method: 'db.revisions', params: { since } }) + return this.getDump() } async getDump (): Promise { - return this.http.rpcRequest({ method: 'db.dump' }) + const cache = await this.bootstrapper.init() + return { + id: cache.sequence, + value: cache.data, + expireId: null, + } } async setDbValueRaw (params: RR.SetDBValueReq): Promise { await pauseFor(2000) - return this.http.rpcRequest>({ method: 'db.put.ui', params }) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/ui' + params.pointer, + value: params.value, + }, + ] + return this.withRevision(patch) } // auth async login (params: RR.LoginReq): Promise { await pauseFor(2000) + this.mockPatch$.next({ id: 1, value: mockPatchData, expireId: null }) return null } @@ -71,7 +89,8 @@ export class MockApiService extends ApiService { value: params.value, }, ] - return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + + return this.withRevision(patch) } async getServerLogs (params: RR.GetServerLogsReq): Promise { @@ -107,6 +126,10 @@ export class MockApiService extends ApiService { downloaded: 0, } + setTimeout(() => { + this.updateOSProgress(initialProgress.size) + }, 500) + const patch = [ { op: PatchOp.REPLACE, @@ -119,12 +142,8 @@ export class MockApiService extends ApiService { value: initialProgress, }, ] - const res = await this.http.rpcRequest({ method: 'db.patch', params: { patch } }) - res.response = 'updating' - this.updateOSProgress(initialProgress.size) - - return res + return this.withRevision(patch, 'updating') } async restartServer (params: RR.RestartServerReq): Promise { @@ -198,11 +217,8 @@ export class MockApiService extends ApiService { value: 0, }, ] - const { revision } = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) as WithRevision - return { - response: Mock.Notifications, - revision, - } + + return this.withRevision(patch, Mock.Notifications) } async deleteNotification (params: RR.DeleteNotificationReq): Promise { @@ -264,55 +280,48 @@ export class MockApiService extends ApiService { async createBackupRaw (params: RR.CreateBackupReq): Promise { await pauseFor(2000) const path = '/server-info/status' - let patch = [ - { - op: PatchOp.REPLACE, - path, - value: ServerStatus.BackingUp, - }, - ] - const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - const ids = ['bitcoind', 'lnd'] setTimeout(async () => { for (let i = 0; i < ids.length; i++) { const appPath = `/package-data/${ids[i]}/installed/status/main/status` - let appPatch = [ + const appPatch = [ { op: PatchOp.REPLACE, path: appPath, value: PackageMainStatus.BackingUp, }, ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch: appPatch } }) + this.updateMock(appPatch) await pauseFor(8000) - appPatch = [ - { - op: PatchOp.REPLACE, - path: appPath, - value: PackageMainStatus.Stopped, - }, - ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch: appPatch } }) + appPatch[0].value = PackageMainStatus.Stopped + this.updateMock(appPatch) } await pauseFor(1000) // set server back to running - patch = [ + const lastPatch = [ { op: PatchOp.REPLACE, path, value: ServerStatus.Running, }, ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - }, 200) + this.updateMock(lastPatch) + }, 500) - return res + const originalPatch = [ + { + op: PatchOp.REPLACE, + path, + value: ServerStatus.BackingUp, + }, + ] + + return this.withRevision(originalPatch) } // drives @@ -362,6 +371,10 @@ export class MockApiService extends ApiService { 'unpack-complete': false, } + setTimeout(async () => { + this.updateProgress(params.id, initialProgress) + }, 1000) + const pkg: PackageDataEntry = { ...Mock.LocalPkgs[params.id], state: PackageState.Installing, @@ -376,12 +389,7 @@ export class MockApiService extends ApiService { value: pkg, }, ] - - const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - setTimeout(async () => { - this.updateProgress(params.id, initialProgress) - }, 1000) - return res + return this.withRevision(patch) } async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise { @@ -411,7 +419,7 @@ export class MockApiService extends ApiService { value: true, }, ] - return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + return this.withRevision(patch) } async restorePackagesRaw (params: RR.RestorePackagesReq): Promise { @@ -440,12 +448,13 @@ export class MockApiService extends ApiService { }, 2000) return { - op: 'add', + op: PatchOp.ADD, path: `/package-data/${id}`, value: pkg, } }) - return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + + return this.withRevision(patch) } async executePackageAction (params: RR.ExecutePackageActionReq): Promise { @@ -459,18 +468,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) setTimeout(async () => { - const patch0 = [ - { - op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Starting, - }, - ] - await this.http.rpcRequest>({ method: 'db.patch', params: { patch: patch0 } }) - - await pauseFor(4000) - console.log('run') - const patch1 = [ + const patch2 = [ { op: PatchOp.REPLACE, path: path + '/status', @@ -482,9 +480,9 @@ export class MockApiService extends ApiService { value: new Date().toISOString(), }, ] - await this.http.rpcRequest>({ method: 'db.patch', params: { patch: patch1 } }) + this.updateMock(patch2) - const patch2 = [ + const patch3 = [ { op: PatchOp.REPLACE, path: path + '/health', @@ -498,12 +496,11 @@ export class MockApiService extends ApiService { }, }, ] - - await this.http.rpcRequest>({ method: 'db.patch', params: { patch: patch2 } }) + this.updateMock(patch3) await pauseFor(2000) - const patch3 = [ + const patch4 = [ { op: PatchOp.REPLACE, path: path + '/health', @@ -528,11 +525,18 @@ export class MockApiService extends ApiService { }, }, ] - - await this.http.rpcRequest>({ method: 'db.patch', params: { patch: patch3 } }) + this.updateMock(patch4) }, 2000) - return { response: null} + const originalPatch = [ + { + op: PatchOp.REPLACE, + path: path + '/status', + value: PackageMainStatus.Starting, + }, + ] + + return this.withRevision(originalPatch) } async dryStopPackage (params: RR.DryStopPackageReq): Promise { @@ -550,6 +554,18 @@ export class MockApiService extends ApiService { async stopPackageRaw (params: RR.StopPackageReq): Promise { await pauseFor(2000) const path = `/package-data/${params.id}/installed/status/main` + + setTimeout(() => { + const patch2 = [ + { + op: PatchOp.REPLACE, + path: path + '/status', + value: PackageMainStatus.Stopped, + }, + ] + this.updateMock(patch2) + }, this.revertTime) + const patch = [ { op: PatchOp.REPLACE, @@ -562,19 +578,8 @@ export class MockApiService extends ApiService { value: { }, }, ] - const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - setTimeout(() => { - const patch = [ - { - op: PatchOp.REPLACE, - path: path + '/status', - value: PackageMainStatus.Stopped, - }, - ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - }, this.revertTime) - return res + return this.withRevision(patch) } async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise { @@ -584,6 +589,17 @@ export class MockApiService extends ApiService { async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise { await pauseFor(2000) + + setTimeout(async () => { + const patch2 = [ + { + op: PatchOp.REMOVE, + path: `/package-data/${params.id}`, + } as RemoveOperation, + ] + this.updateMock(patch2) + }, this.revertTime) + const patch = [ { op: PatchOp.REPLACE, @@ -591,35 +607,19 @@ export class MockApiService extends ApiService { value: PackageState.Removing, }, ] - let res: any - res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - setTimeout(async () => { - const patch = [ - { - op: PatchOp.REMOVE, - path: `/package-data/${params.id}`, - }, - ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - }, this.revertTime) - - return res + return this.withRevision(patch) } - async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise { + async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise { await pauseFor(2000) - let res: any - const patch = [ { op: PatchOp.REMOVE, path: `/recovered-packages/${params.id}`, - }, + } as RemoveOperation, ] - res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - - return res + return this.withRevision(patch) } async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise { @@ -631,7 +631,7 @@ export class MockApiService extends ApiService { } } - private async updateProgress (id: string, initialProgress: InstallProgress) { + private async updateProgress (id: string, initialProgress: InstallProgress): Promise { const phases = [ { progress: 'downloaded', completion: 'download-complete'}, { progress: 'validated', completion: 'validation-complete'}, @@ -653,10 +653,10 @@ export class MockApiService extends ApiService { op: PatchOp.REMOVE, path: `/package-data/${id}/install-progress`, }, - { - op: PatchOp.REMOVE, - path: `/recovered-packages/${id}`, - }, + // { + // op: PatchOp.REMOVE, + // path: `/recovered-packages/${id}`, + // } as RemoveOperation, ] } else { patch = [ @@ -667,23 +667,19 @@ export class MockApiService extends ApiService { }, ] } - try { - await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - } catch (e) { - console.error('Insufficient Mocks, happens when installing a service that does not exist in recovered-package') - } + this.updateMock(patch) } } setTimeout(() => { - const patch = [ + const patch2 = [ { op: PatchOp.REPLACE, path: `/package-data/${id}`, value: Mock.LocalPkgs[id], }, ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + this.updateMock(patch2) }, 1000) } @@ -699,20 +695,20 @@ export class MockApiService extends ApiService { value: downloaded, }, ] - await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + this.updateMock(patch) } - const patch = [ + const patch2 = [ { op: PatchOp.REPLACE, path: `/server-info/update-progress/downloaded`, value: size, }, ] - await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + this.updateMock(patch2) setTimeout(async () => { - const patch = [ + const patch3: Operation[] = [ { op: PatchOp.REPLACE, path: '/server-info/status', @@ -723,18 +719,48 @@ export class MockApiService extends ApiService { path: '/server-info/update-progress', }, ] - - await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + this.updateMock(patch3) // quickly revert server to "running" for continued testing - const patch2 = [ + await pauseFor(100) + const patch4 = [ { op: PatchOp.REPLACE, path: '/server-info/status', value: ServerStatus.Running, }, ] - this.http.rpcRequest>({ method: 'db.patch', params: { patch: patch2 } }) + this.updateMock(patch4) }, 1000) + } + private async updateMock (patch: Operation[]): Promise { + if (!this.sequence) { + const { sequence } = await this.bootstrapper.init() + this.sequence = sequence + } + const revision = { + id: ++this.sequence, + patch, + expireId: null, + } + console.log('REVISION', revision) + this.mockPatch$.next(revision) + } + + private async withRevision (patch: Operation[], response: T = null): Promise> { + if (!this.sequence) { + const { sequence } = await this.bootstrapper.init() + this.sequence = sequence + } + + const revision = { + id: ++this.sequence, + patch, + expireId: null, + } + + console.log('REVISION', revision) + + return { response, revision } } } diff --git a/ui/src/app/services/api/mock-patch.ts b/ui/src/app/services/api/mock-patch.ts new file mode 100644 index 000000000..c4654fae7 --- /dev/null +++ b/ui/src/app/services/api/mock-patch.ts @@ -0,0 +1,614 @@ +import { DataModel, DependencyErrorType, DependencySeverity, DockerIoFormat, HealthResult, Manifest, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model' + +export const mockPatchData: DataModel = { + 'ui': { + 'name': 'Matt\'s Embassy', + 'auto-check-updates': true, + 'pkg-order': [], + 'ack-welcome': '1.0.0', + 'ack-share-stats': false, + }, + 'server-info': { + 'id': 'embassy-abcdefgh', + 'version': '0.3.0', + 'last-backup': null, + 'status': ServerStatus.Running, + 'lan-address': 'https://embassy-abcdefgh.local', + 'tor-address': 'http://myveryownspecialtoraddress.onion', + 'eos-marketplace': 'https://beta-registry-0-3.start9labs.com', + 'package-marketplace': null, + 'share-stats': false, + 'unread-notification-count': 4, + // 'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + 'eos-version-compat': '>=0.3.0', + }, + 'recovered-packages': { + 'btc-rpc-proxy': { + 'title': 'Bitcoin Proxy', + 'icon': 'assets/img/service-icons/btc-rpc-proxy.png', + 'version': '0.2.2', + }, + }, + 'package-data': { + 'bitcoind': { + 'state': PackageState.Installed, + 'static-files': { + 'license': '/public/package-data/bitcoind/0.20.0/LICENSE.md', + 'icon': '/assets/img/service-icons/bitcoind.png', + 'instructions': '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md', + }, + 'manifest': { + 'id': 'bitcoind', + 'title': 'Bitcoin Core', + 'version': '0.20.0', + 'description': { + 'short': 'A Bitcoin full node by Bitcoin Core.', + 'long': 'Bitcoin is a decentralized consensus protocol and settlement network.', + }, + 'release-notes': 'Taproot, Schnorr, and more.', + 'license': 'MIT', + 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', + 'upstream-repo': 'https://github.com/bitcoin/bitcoin', + 'support-site': 'https://bitcoin.org', + 'marketing-site': 'https://bitcoin.org', + 'donation-url': 'https://start9.com', + 'alerts': { + 'install': 'Bitcoin can take over a week to sync.', + 'uninstall': 'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.', + 'restore': null, + 'start': null, + 'stop': 'Stopping Bitcoin is bad for your health.', + }, + 'main': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + 'health-checks': { + 'chain-state': { 'name': 'Chain State', 'description': 'Checks the chainstate' }, + 'ephemeral-health-check': { 'name': 'Ephemeral Health Check', 'description': 'Checks to see if your new user registrations are on. If they are but you\'re not expecting any new user signups, you should disable this in Config, as anyone who knows your onion URL can create accounts on your server.' }, + 'p2p-interface': { 'name': 'P2P Interface', 'description': 'Checks to see if your new user registrations are on. If they are but you\'re not expecting any new user signups, you should disable this in Config, as anyone who knows your onion URL can create accounts on your server.' }, + 'rpc-interface': { 'name': 'RPC Interface', 'description': 'Checks the RPC Interface' }, + 'unnecessary-health-check': { 'name': 'Unneccessary Health Check', 'description': 'Is totally not necessary to do this health check.' }, + } as any, + 'config': { + 'get': { }, + 'set': { }, + } as any, + 'volumes': { }, + 'min-os-version': '0.2.12', + 'interfaces': { + 'ui': { + 'name': 'Node Visualizer', + 'description': 'Web application for viewing information about your node and the Bitcoin network.', + 'ui': true, + 'tor-config': { + 'port-mapping': { }, + }, + 'lan-config': { }, + 'protocols': [], + }, + 'rpc': { + 'name': 'RPC', + 'description': 'Used by wallets to interact with your Bitcoin Core node.', + 'ui': false, + 'tor-config': { + 'port-mapping': { }, + }, + 'lan-config': { }, + 'protocols': [], + }, + 'p2p': { + 'name': 'P2P', + 'description': 'Used by other Bitcoin nodes to communicate and interact with your node.', + 'ui': false, + 'tor-config': { + 'port-mapping': { }, + }, + 'lan-config': { }, + 'protocols': [], + }, + }, + 'backup': { + 'create': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + 'restore': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + }, + 'migrations': null, + 'actions': { + 'resync': { + 'name': 'Resync Blockchain', + 'description': 'Use this to resync the Bitcoin blockchain from genesis', + 'warning': 'This will take a couple of days.', + 'allowed-statuses': [ + PackageMainStatus.Running, + PackageMainStatus.Stopped, + ], + 'implementation': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + 'input-spec': { + 'reason': { + 'type': 'string', + 'name': 'Re-sync Reason', + 'description': 'Your reason for re-syncing. Why are you doing this?', + 'nullable': false, + 'masked': false, + 'copyable': false, + 'pattern': '^[a-zA-Z]+$', + 'pattern-description': 'Must contain only letters.', + }, + 'name': { + 'type': 'string', + 'name': 'Your Name', + 'description': 'Tell the class your name.', + 'nullable': true, + 'masked': false, + 'copyable': false, + 'pattern': null, + 'pattern-description': null, + 'warning': 'You may loose all your money by providing your name.', + }, + 'notifications': { + 'name': 'Notification Preferences', + 'type': 'list', + 'subtype': 'enum', + 'description': 'how you want to be notified', + 'range': '[1,3]', + 'default': [ + 'email', + ], + 'spec': { + 'value-names': { + 'email': 'Email', + 'text': 'Text', + 'call': 'Call', + 'push': 'Push', + 'webhook': 'Webhook', + }, + 'values': [ + 'email', + 'text', + 'call', + 'push', + 'webhook', + ], + }, + }, + 'days-ago': { + 'type': 'number', + 'name': 'Days Ago', + 'description': 'Number of days to re-sync.', + 'nullable': false, + 'default': 100, + 'range': '[0, 9999]', + 'integral': true, + }, + 'top-speed': { + 'type': 'number', + 'name': 'Top Speed', + 'description': 'The fastest you can possibly run.', + 'nullable': false, + 'default': null, + 'range': '[-1000, 1000]', + 'integral': false, + 'units': 'm/s', + }, + 'testnet': { + 'name': 'Testnet', + 'type': 'boolean', + 'description': 'determines whether your node is running on testnet or mainnet', + 'warning': 'Chain will have to resync!', + 'default': false, + }, + 'randomEnum': { + 'name': 'Random Enum', + 'type': 'enum', + 'value-names': { + 'null': 'Null', + 'good': 'Good', + 'bad': 'Bad', + 'ugly': 'Ugly', + }, + 'default': 'null', + 'description': 'This is not even real.', + 'warning': 'Be careful changing this!', + 'values': [ + 'null', + 'good', + 'bad', + 'ugly', + ], + }, + 'emergency-contact': { + 'name': 'Emergency Contact', + 'type': 'object', + 'unique-by': null, + 'description': 'The person to contact in case of emergency.', + 'spec': { + 'name': { + 'type': 'string', + 'name': 'Name', + 'description': null, + 'nullable': false, + 'masked': false, + 'copyable': false, + 'pattern': '^[a-zA-Z]+$', + 'pattern-description': 'Must contain only letters.', + }, + 'email': { + 'type': 'string', + 'name': 'Email', + 'description': null, + 'nullable': false, + 'masked': false, + 'copyable': true, + }, + }, + }, + 'ips': { + 'name': 'Whitelist IPs', + 'type': 'list', + 'subtype': 'string', + 'description': 'external ip addresses that are authorized to access your Bitcoin node', + 'warning': 'Any IP you allow here will have RPC access to your Bitcoin node.', + 'range': '[1,10]', + 'default': [ + '192.168.1.1', + ], + 'spec': { + 'pattern': '^[0-9]{1,3}([,.][0-9]{1,3})?$', + 'pattern-description': 'Must be a valid IP address', + 'masked': false, + 'copyable': false, + }, + }, + 'bitcoinNode': { + 'name': 'Bitcoin Node Settings', + 'type': 'union', + 'unique-by': null, + 'description': 'The node settings', + 'default': 'internal', + 'warning': 'Careful changing this', + 'tag': { + 'id': 'type', + 'name': 'Type', + 'variant-names': { + 'internal': 'Internal', + 'external': 'External', + }, + }, + 'variants': { + 'internal': { + 'lan-address': { + 'name': 'LAN Address', + 'type': 'pointer', + 'subtype': 'package', + 'target': 'lan-address', + 'package-id': 'bitcoind', + 'description': 'the lan address', + 'interface': '', + }, + 'friendly-name': { + 'name': 'Friendly Name', + 'type': 'string', + 'description': 'the lan address', + 'nullable': true, + 'masked': false, + 'copyable': false, + }, + }, + 'external': { + 'public-domain': { + 'name': 'Public Domain', + 'type': 'string', + 'description': 'the public address of the node', + 'nullable': false, + 'default': 'bitcoinnode.com', + 'pattern': '.*', + 'pattern-description': 'anything', + 'masked': false, + 'copyable': true, + }, + }, + }, + }, + }, + }, + }, + 'permissions': { }, + 'dependencies': { }, + }, + 'installed': { + 'manifest': { } as Manifest, + 'last-backup': null, + 'status': { + 'configured': true, + 'main': { + 'status': PackageMainStatus.Running, + 'started': '2021-06-14T20:49:17.774Z', + 'health': { + 'ephemeral-health-check': { + 'result': HealthResult.Starting, + }, + 'chain-state': { + 'result': HealthResult.Loading, + 'message': 'Bitcoin is syncing from genesis', + }, + 'p2p-interface': { + 'result': HealthResult.Success, + }, + 'rpc-interface': { + 'result': HealthResult.Failure, + 'error': 'RPC interface unreachable.', + }, + 'unnecessary-health-check': { + 'result': HealthResult.Disabled, + }, + }, + }, + 'dependency-errors': { }, + }, + 'interface-addresses': { + 'ui': { + 'tor-address': 'bitcoind-ui-address.onion', + 'lan-address': 'bitcoind-ui-address.local', + }, + 'rpc': { + 'tor-address': 'bitcoind-rpc-address.onion', + 'lan-address': 'bitcoind-rpc-address.local', + }, + 'p2p': { + 'tor-address': 'bitcoind-p2p-address.onion', + 'lan-address': 'bitcoind-p2p-address.local', + }, + }, + 'system-pointers': [], + 'current-dependents': { + 'lnd': { + 'pointers': [], + 'health-checks': [], + }, + }, + 'current-dependencies': { }, + 'dependency-info': { }, + }, + }, + 'lnd': { + 'state': PackageState.Installed, + 'static-files': { + 'license': '/public/package-data/lnd/0.11.1/LICENSE.md', + 'icon': '/assets/img/service-icons/lnd.png', + 'instructions': '/public/package-data/lnd/0.11.1/INSTRUCTIONS.md', + }, + 'manifest': { + 'id': 'lnd', + 'title': 'Lightning Network Daemon', + 'version': '0.11.1', + 'description': { + 'short': 'A bolt spec compliant client.', + 'long': 'More info about LND. More info about LND. More info about LND.', + }, + 'release-notes': 'Dual funded channels!', + 'license': 'MIT', + 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', + 'upstream-repo': 'https://github.com/lightningnetwork/lnd', + 'support-site': 'https://lightning.engineering/', + 'marketing-site': 'https://lightning.engineering/', + 'donation-url': null, + 'alerts': { + 'install': null, + 'uninstall': null, + 'restore': 'If this is a duplicate instance of the same LND node, you may loose your funds.', + 'start': 'Starting LND is good for your health.', + 'stop': null, + }, + 'main': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + 'health-checks': { }, + 'config': { + 'get': null, + 'set': null, + }, + 'volumes': { }, + 'min-os-version': '0.2.12', + 'interfaces': { + 'rpc': { + 'name': 'RPC interface', + 'description': 'Good for connecting to your node at a distance.', + 'ui': true, + 'tor-config': { + 'port-mapping': { }, + }, + 'lan-config': { + '44': { + 'ssl': true, + 'mapping': 33, + }, + }, + 'protocols': [], + }, + 'grpc': { + 'name': 'GRPC', + 'description': 'Certain wallet use grpc.', + 'ui': false, + 'tor-config': { + 'port-mapping': { }, + }, + 'lan-config': { + '66': { + 'ssl': true, + 'mapping': 55, + }, + }, + 'protocols': [], + }, + }, + 'backup': { + 'create': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + 'restore': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + }, + 'migrations': null, + 'actions': { + 'resync': { + 'name': 'Resync Network Graph', + 'description': 'Your node will resync its network graph.', + 'warning': 'This will take a couple hours.', + 'allowed-statuses': [ + PackageMainStatus.Running, + ], + 'implementation': { + 'type': 'docker', + 'image': '', + 'system': true, + 'entrypoint': '', + 'args': [], + 'mounts': { }, + 'io-format': DockerIoFormat.Yaml, + 'inject': false, + 'shm-size': '', + }, + 'input-spec': null, + }, + }, + 'permissions': { }, + 'dependencies': { + 'bitcoind': { + 'version': '=0.21.0', + 'description': 'LND needs bitcoin to live.', + 'requirement': { + 'type': 'opt-out', + 'how': 'You can use an external node from your Embassy if you prefer.', + }, + 'config': null, + 'severity': DependencySeverity.Critical, + }, + 'btc-rpc-proxy': { + 'version': '>=0.2.2', + 'description': 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', + 'requirement': { + 'type': 'opt-in', + 'how': 'To use Proxy\'s user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.', + }, + 'config': null, + 'severity': DependencySeverity.Warning, + }, + }, + }, + 'installed': { + 'manifest': { } as Manifest, + 'last-backup': null, + 'status': { + 'configured': true, + 'main': { + 'status': PackageMainStatus.Stopped, + }, + 'dependency-errors': { + 'btc-rpc-proxy': { + 'type': DependencyErrorType.ConfigUnsatisfied, + 'error': 'This is a config unsatisfied error', + }, + }, + }, + 'interface-addresses': { + 'rpc': { + 'tor-address': 'lnd-rpc-address.onion', + 'lan-address': 'lnd-rpc-address.local', + }, + 'grpc': { + 'tor-address': 'lnd-grpc-address.onion', + 'lan-address': 'lnd-grpc-address.local', + }, + }, + 'system-pointers': [], + 'current-dependents': { }, + 'current-dependencies': { + 'bitcoind': { + 'pointers': [], + 'health-checks': [], + }, + 'btc-rpc-proxy': { + 'pointers': [], + 'health-checks': [], + }, + }, + 'dependency-info': { + 'bitcoind': { + 'manifest': { + 'title': 'Bitcoin Core', + } as Manifest, + 'icon': 'assets/img/service-icons/bitcoind.png', + }, + 'btc-rpc-proxy': { + 'manifest': { + 'title': 'Bitcoin Proxy', + } as Manifest, + 'icon': 'assets/img/service-icons/btc-rpc-proxy.png', + }, + }, + }, + }, + }, +} \ No newline at end of file diff --git a/ui/src/app/services/patch-db/patch-db.factory.ts b/ui/src/app/services/patch-db/patch-db.factory.ts index 3a5534265..e2c652916 100644 --- a/ui/src/app/services/patch-db/patch-db.factory.ts +++ b/ui/src/app/services/patch-db/patch-db.factory.ts @@ -1,10 +1,13 @@ -import { PollSource, Source, WebsocketSource } from 'patch-db-client' +import { MockSource, PollSource, Source, WebsocketSource } from 'patch-db-client' import { ConfigService } from 'src/app/services/config.service' import { DataModel } from './data-model' import { LocalStorageBootstrap } from './local-storage-bootstrap' import { PatchDbService } from './patch-db.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from '../auth.service' +import { MockApiService } from '../api/embassy-mock-api.service' +import { filter } from 'rxjs/operators' +import { exists } from 'src/app/util/misc.util' export function PatchDbServiceFactory ( config: ConfigService, @@ -18,11 +21,7 @@ export function PatchDbServiceFactory ( let source: Source if (mocks.enabled) { - if (mocks.connection === 'poll') { - source = new PollSource({ ...poll }, embassyApi) - } else { - source = new WebsocketSource(`ws://localhost:${config.mocks.wsPort}/db`) - } + source = new MockSource((embassyApi as MockApiService).mockPatch$.pipe(filter(exists))) } else { if (!supportsWebSockets) { source = new PollSource({ ...poll }, embassyApi)