From e8bf254b91f63d86382d7b900650fe902c87b327 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 16 Jul 2021 17:58:53 -0600 Subject: [PATCH] refine startup alerts, reversions in mocks --- ui/package-lock.json | 4 +- ui/package.json | 2 +- ui/src/app/app.component.ts | 81 ++++- ui/src/app/guards/maintenance.guard.ts | 1 - ui/src/app/guards/unmaintenance.guard.ts | 4 +- .../modals/os-welcome/os-welcome.page.html | 4 +- ui/src/app/pages/login/login.page.ts | 29 +- .../pages/maintenance/maintenance.page.html | 4 +- ui/src/app/services/api/api.fixures.ts | 331 +++++++++--------- .../api/embassy/embassy-api.service.ts | 2 +- .../api/embassy/embassy-live-api.service.ts | 6 +- .../api/embassy/embassy-mock-api.service.ts | 146 +++++--- .../marketplace-live-api.service.ts | 48 ++- .../marketplace-mock-api.service.ts | 62 ++-- ui/src/app/services/connection.service.ts | 7 +- ui/src/app/services/http.service.ts | 35 +- .../app/services/patch-db/patch-db.service.ts | 4 - ui/src/app/services/startup-alerts.service.ts | 17 +- 18 files changed, 473 insertions(+), 314 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 69c275baf..afefd7a91 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "embassy-ui", - "version": "0.2.14", + "version": "0.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "embassy-ui", - "version": "0.2.14", + "version": "0.3.0", "dependencies": { "@angular/common": "^11.0.0", "@angular/core": "^11.0.0", diff --git a/ui/package.json b/ui/package.json index 30d5fa299..d95699bba 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "embassy-ui", - "version": "0.2.14", + "version": "0.3.0", "description": "GUI for EmbassyOS", "author": "Start9 Labs", "homepage": "https://github.com/Start9Labs/embassy-ui", diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index f37485c7e..c47740c7b 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -3,7 +3,7 @@ import { Storage } from '@ionic/storage' import { AuthService, AuthState } from './services/auth.service' import { ApiService } from './services/api/embassy/embassy-api.service' import { Router, RoutesRecognized } from '@angular/router' -import { debounceTime, distinctUntilChanged, filter, finalize, takeWhile } from 'rxjs/operators' +import { debounceTime, distinctUntilChanged, filter, finalize, skip, take, takeWhile } from 'rxjs/operators' import { AlertController, IonicSafeString, ToastController } from '@ionic/angular' import { LoaderService } from './services/loader.service' import { Emver } from './services/emver.service' @@ -14,6 +14,8 @@ import { HttpService } from './services/http.service' import { ServerStatus } from './services/patch-db/data-model' import { ConnectionFailure, ConnectionService } from './services/connection.service' import { StartupAlertsService } from './services/startup-alerts.service' +import { ConfigService } from './services/config.service' +import { isEmptyObject } from './util/misc.util' @Component({ selector: 'app-root', @@ -63,6 +65,7 @@ export class AppComponent { private readonly startupAlertsService: StartupAlertsService, private readonly toastCtrl: ToastController, private readonly patch: PatchDbService, + private readonly config: ConfigService, readonly splitPane: SplitPaneTracker, ) { // set dark theme @@ -83,23 +86,33 @@ export class AppComponent { .subscribe(auth => { // VERIFIED if (auth === AuthState.VERIFIED) { - this.http.authReqEnabled = true - this.showMenu = true this.patch.start() - this.connectionService.start() - // watch connection to display connectivity issues - this.watchConnection(auth) - // watch router to highlight selected menu item - this.watchRouter(auth) - // watch status to display/hide maintenance page - this.watchStatus(auth) - // watch unread notification count to display toast - this.watchNotifications(auth) - // run startup alerts - this.startupAlertsService.runChecks() + + this.patch.watch$() + .pipe( + filter(data => !isEmptyObject(data)), + take(1), + ) + .subscribe(_ => { + this.showMenu = true + this.router.navigate([''], { replaceUrl: true }) + this.connectionService.start() + // watch connection to display connectivity issues + this.watchConnection(auth) + // watch router to highlight selected menu item + this.watchRouter(auth) + // watch status to display/hide maintenance page + this.watchStatus(auth) + // watch version to refresh browser window + this.watchVersion(auth) + // watch unread notification count to display toast + this.watchNotifications(auth) + // run startup alerts + this.startupAlertsService.runChecks() + }) + // UNVERIFIED } else if (auth === AuthState.UNVERIFIED) { - this.http.authReqEnabled = false this.showMenu = false this.connectionService.stop() this.patch.stop() @@ -178,16 +191,30 @@ export class AppComponent { ) .subscribe(status => { const maintenance = '/maintenance' - const url = this.router.url - if (status === ServerStatus.Running && url.startsWith(maintenance)) { + const route = this.router.url + console.log('STATUS', status, 'URL', route) + if (status === ServerStatus.Running && route.startsWith(maintenance)) { this.router.navigate([''], { replaceUrl: true }) } - if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(status) && !url.startsWith(maintenance)) { + if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(status) && !route.startsWith(maintenance)) { + this.showMenu = false this.router.navigate([maintenance], { replaceUrl: true }) } }) } + private watchVersion (auth: AuthState): void { + this.patch.watch$('server-info', 'version') + .pipe( + takeWhile(() => auth === AuthState.VERIFIED), + ) + .subscribe(version => { + if (this.emver.compare(this.config.version, version) !== 0) { + this.presentAlertRefreshNeeded() + } + }) + } + private watchNotifications (auth: AuthState): void { let previous: number this.patch.watch$('server-info', 'unread-notification-count') @@ -202,7 +229,25 @@ export class AppComponent { }) } + async presentAlertRefreshNeeded () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Refresh Needed', + message: 'Your EmbassyOS UI is out of date. Hard refresh the page to get the latest UI.', + buttons: [ + { + text: 'Refresh Page', + handler: () => { + location.reload() + }, + }, + ], + }) + await alert.present() + } + async presentAlertLogout () { + // @TODO warn user no way to recover Embassy if logout and forget password. Maybe require password to logout? const alert = await this.alertCtrl.create({ backdropDismiss: false, header: 'Caution', diff --git a/ui/src/app/guards/maintenance.guard.ts b/ui/src/app/guards/maintenance.guard.ts index b234b48af..206e7eaa1 100644 --- a/ui/src/app/guards/maintenance.guard.ts +++ b/ui/src/app/guards/maintenance.guard.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core' import { CanActivate, Router, CanActivateChild } from '@angular/router' -import { tap } from 'rxjs/operators' import { ServerStatus } from '../services/patch-db/data-model' import { PatchDbService } from '../services/patch-db/patch-db.service' diff --git a/ui/src/app/guards/unmaintenance.guard.ts b/ui/src/app/guards/unmaintenance.guard.ts index 884946ba2..aa10dd0e4 100644 --- a/ui/src/app/guards/unmaintenance.guard.ts +++ b/ui/src/app/guards/unmaintenance.guard.ts @@ -14,8 +14,8 @@ export class UnmaintenanceGuard implements CanActivate { private readonly router: Router, private readonly patch: PatchDbService, ) { - this.patch.sequence$.subscribe(_ => { - this.serverStatus = this.patch.data['server-info'].status + this.patch.watch$('server-info', 'status').subscribe(status => { + this.serverStatus = status }) } diff --git a/ui/src/app/modals/os-welcome/os-welcome.page.html b/ui/src/app/modals/os-welcome/os-welcome.page.html index dfdb6e784..8558ffb0b 100644 --- a/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -1,7 +1,7 @@ - Welcome to 0.2.14! + Welcome to {{ version }}! @@ -14,7 +14,7 @@
- + Begin
diff --git a/ui/src/app/pages/login/login.page.ts b/ui/src/app/pages/login/login.page.ts index 5f50be739..f0efb80d1 100644 --- a/ui/src/app/pages/login/login.page.ts +++ b/ui/src/app/pages/login/login.page.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core' -import { NavController } from '@ionic/angular' +import { LoadingController } from '@ionic/angular' import { AuthService } from 'src/app/services/auth.service' -import { LoaderService } from 'src/app/services/loader.service' @Component({ selector: 'login', @@ -12,15 +11,18 @@ export class LoginPage { password = '' unmasked = false error = '' + loader: HTMLIonLoadingElement constructor ( private readonly authService: AuthService, - private readonly loader: LoaderService, - private readonly navCtrl: NavController, + private readonly loadingCtrl: LoadingController, ) { } - ionViewDidEnter () { - this.error = '' + ngOnDestroy () { + if (this.loader) { + this.loader.dismiss() + this.loader = undefined + } } toggleMask () { @@ -28,14 +30,21 @@ export class LoginPage { } async submit () { + this.error = '' + + this.loader = await this.loadingCtrl.create({ + message: 'Authenticating', + spinner: 'lines', + }) + await this.loader.present() + try { - await this.loader.displayDuringP( - this.authService.login(this.password), - ) + await this.authService.login(this.password) + this.loader.message = 'Loading Embassy Data' this.password = '' - await this.navCtrl.navigateForward(['/']) } catch (e) { this.error = e.message + this.loader.dismiss() } } } diff --git a/ui/src/app/pages/maintenance/maintenance.page.html b/ui/src/app/pages/maintenance/maintenance.page.html index f9717e20d..b7258715e 100644 --- a/ui/src/app/pages/maintenance/maintenance.page.html +++ b/ui/src/app/pages/maintenance/maintenance.page.html @@ -1,10 +1,10 @@ - + -

Updating Embassy

+

Embassy is updating

Embassy is backing up

diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index c6a80ea4a..c2cc6315f 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -1,12 +1,15 @@ -import { DependencyErrorType, DockerIoFormat, Manifest, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model' -import { MarketplacePkg, Metric, NotificationLevel, RR, ServerNotification, ServerNotifications } from './api.types' +import { DockerIoFormat, Manifest, PackageDataEntry, PackageMainStatus, PackageState } from 'src/app/services/patch-db/data-model' +import { MarketplacePkg, Metric, NotificationLevel, RR, ServerNotifications } from './api.types' export module Mock { export const MarketplaceEos: RR.GetMarketplaceEOSRes = { - version: '1.0.0', + version: '0.3.1', headline: 'Our biggest release ever.', - 'release-notes': { '1.0.0': 'Some **Markdown** release _notes_' }, + 'release-notes': { + '0.3.1': 'Some **Markdown** release _notes_ for 0.3.1', + '0.3.0': 'Some **Markdown** release _notes_ from a prior version', + }, } export const ReleaseNotes: RR.GetReleaseNotesRes = { @@ -375,7 +378,7 @@ export module Mock { }, } - export const AvailableShow: { + export const MarketplacePkgs: { [id: string]: { [version: string]: MarketplacePkg } @@ -513,108 +516,7 @@ export module Mock { }, } - export const AvailableList: RR.GetMarketplacePackagesRes = Object.values(Mock.AvailableShow).map(service => service['latest']) - - export const bitcoind: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: 'licenseUrl', // /public/package-data/bitcoind/0.21.1/LICENSE.md, - icon: 'assets/img/service-icons/bitcoind.png', - instructions: 'instructionsUrl', // /public/package-data/bitcoind/0.21.1/INSTRUCTIONS.md - }, - manifest: { - ...MockManifestBitcoind, - version: '0.20.0', - }, - installed: { - status: { - configured: true, - main: { - status: PackageMainStatus.Running, - started: new Date().toISOString(), - health: { }, - }, - 'dependency-errors': { }, - }, - 'interface-info': { - ip: '10.0.0.1', - 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': { }, - }, - 'install-progress': undefined, - } - - export const lnd: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: 'licenseUrl', // /public/package-data/lnd/0.21.1/LICENSE.md, - icon: 'assets/img/service-icons/lnd.png', - instructions: 'instructionsUrl', // /public/package-data/lnd/0.21.1/INSTRUCTIONS.md - }, - manifest: MockManifestLnd, - installed: { - status: { - configured: true, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-errors': { - 'bitcoin-proxy': { - type: DependencyErrorType.NotInstalled, - title: Mock.MockManifestBitcoinProxy.title, - icon: 'assets/img/service-icons/bitcoin-proxy.png', - }, - }, - }, - 'interface-info': { - ip: '10.0.0.1', - 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': [], - }, - 'bitcoin-proxy': { - pointers: [], - 'health-checks': [], - }, - }, - }, - 'install-progress': undefined, - } + export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = Object.values(Mock.MarketplacePkgs).map(service => service['latest']) export const bitcoinproxy: PackageDataEntry = { state: PackageState.Installed, @@ -660,68 +562,27 @@ export module Mock { 'install-progress': undefined, } - export const DbDump: RR.GetDumpRes = { - id: 1, - expireId: null, - value: { - 'server-info': { - id: 'start9-abcdefgmm', - version: '1.0.0', - status: ServerStatus.Running, - 'lan-address': 'start9-abcdefgh.local', - 'tor-address': 'myveryownspecialtoraddress.onion', - wifi: { - ssids: ['Goosers', 'Goosers5G'], - selected: 'Goosers5G', - connected: 'Goosers5G', - }, - 'package-marketplace': 'https://registry.start9.com', - 'eos-marketplace': 'https://registry.start9.com', - 'unread-notification-count': 4, - specs: { - CPU: 'Cortex-A72: 4 Cores @1500MHz', - Disk: '1TB SSD', - Memory: '8GB', - }, - 'connection-addresses': { - tor: [], - clearnet: [], - }, - }, - 'package-data': { - 'bitcoind': bitcoind, - 'lnd': lnd, - }, - ui: { - 'welcome-ack': '1.0.0', - 'auto-check-updates': true, - }, - }, - } - - export const notification1: ServerNotification<1> = { - id: '123e4567-e89b-12d3-a456-426655440000', - 'package-id': null, - 'created-at': '2019-12-26T14:20:30.872Z', - code: 1, - level: NotificationLevel.Success, - title: 'Backup Complete', - message: 'Embassy and services have been successfully backed up.', - data: { - server: { - attempted: true, - error: null, - }, - packages: { - 'bitcoind': { + export const Notifications: ServerNotifications = [ + { + id: '123e4567-e89b-12d3-a456-426655440000', + 'package-id': null, + 'created-at': '2019-12-26T14:20:30.872Z', + code: 1, + level: NotificationLevel.Success, + title: 'Backup Complete', + message: 'Embassy and services have been successfully backed up.', + data: { + server: { + attempted: true, error: null, }, + packages: { + 'bitcoind': { + error: null, + }, + }, }, }, - } - - export const Notifications: ServerNotifications = [ - notification1, { id: '123e4567-e89b-12d3-a456-426655440001', 'package-id': 'bitcoind', @@ -1435,4 +1296,144 @@ export module Mock { rpcallowip: [], rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], } + + // export const bitcoind: PackageDataEntry = { + // state: PackageState.Installed, + // 'static-files': { + // license: 'licenseUrl', // /public/package-data/bitcoind/0.21.1/LICENSE.md, + // icon: 'assets/img/service-icons/bitcoind.png', + // instructions: 'instructionsUrl', // /public/package-data/bitcoind/0.21.1/INSTRUCTIONS.md + // }, + // manifest: { + // ...MockManifestBitcoind, + // version: '0.20.0', + // }, + // installed: { + // status: { + // configured: true, + // main: { + // status: PackageMainStatus.Running, + // started: new Date().toISOString(), + // health: { }, + // }, + // 'dependency-errors': { }, + // }, + // 'interface-info': { + // ip: '10.0.0.1', + // 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': { }, + // }, + // 'install-progress': undefined, + // } + + // export const lnd: PackageDataEntry = { + // state: PackageState.Installed, + // 'static-files': { + // license: 'licenseUrl', // /public/package-data/lnd/0.21.1/LICENSE.md, + // icon: 'assets/img/service-icons/lnd.png', + // instructions: 'instructionsUrl', // /public/package-data/lnd/0.21.1/INSTRUCTIONS.md + // }, + // manifest: MockManifestLnd, + // installed: { + // status: { + // configured: true, + // main: { + // status: PackageMainStatus.Stopped, + // }, + // 'dependency-errors': { + // 'bitcoin-proxy': { + // type: DependencyErrorType.NotInstalled, + // title: Mock.MockManifestBitcoinProxy.title, + // icon: 'assets/img/service-icons/bitcoin-proxy.png', + // }, + // }, + // }, + // 'interface-info': { + // ip: '10.0.0.1', + // 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': [], + // }, + // 'bitcoin-proxy': { + // pointers: [], + // 'health-checks': [], + // }, + // }, + // }, + // 'install-progress': undefined, + // } + + // export const DbDump: RR.GetDumpRes = { + // id: 1, + // expireId: null, + // value: { + // 'server-info': { + // id: 'start9-abcdefgmm', + // version: '1.0.0', + // status: ServerStatus.Running, + // 'lan-address': 'start9-abcdefgh.local', + // 'tor-address': 'myveryownspecialtoraddress.onion', + // wifi: { + // ssids: ['Goosers', 'Goosers5G'], + // selected: 'Goosers5G', + // connected: 'Goosers5G', + // }, + // 'eos-marketplace': 'https://registry.start9.com', + // 'package-marketplace': 'https://registry.start9.com', + // 'unread-notification-count': 4, + // specs: { + // CPU: 'Cortex-A72: 4 Cores @1500MHz', + // Disk: '1TB SSD', + // Memory: '8GB', + // }, + // 'connection-addresses': { + // tor: ['http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'], + // clearnet: ['https://start9.com'], + // }, + // }, + // 'package-data': { + // 'bitcoind': bitcoind, + // 'lnd': lnd, + // }, + // ui: { + // 'welcome-ack': '1.0.0', + // 'auto-check-updates': true, + // }, + // }, + // } } diff --git a/ui/src/app/services/api/embassy/embassy-api.service.ts b/ui/src/app/services/api/embassy/embassy-api.service.ts index 12ca20593..2d52620eb 100644 --- a/ui/src/app/services/api/embassy/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-api.service.ts @@ -66,7 +66,7 @@ export abstract class ApiService implements Source, Http { // )() // password - abstract updatePassword (params: RR.UpdatePasswordReq): Promise + // abstract updatePassword (params: RR.UpdatePasswordReq): Promise // notification diff --git a/ui/src/app/services/api/embassy/embassy-live-api.service.ts b/ui/src/app/services/api/embassy/embassy-live-api.service.ts index 1a0826fea..10bc5b3d3 100644 --- a/ui/src/app/services/api/embassy/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-live-api.service.ts @@ -83,9 +83,9 @@ export class LiveApiService extends ApiService { // } // password - async updatePassword (params: RR.UpdatePasswordReq): Promise { - return this.http.rpcRequest({ method: 'password.set', params }) - } + // async updatePassword (params: RR.UpdatePasswordReq): Promise { + // return this.http.rpcRequest({ method: 'password.set', params }) + // } // notification diff --git a/ui/src/app/services/api/embassy/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy/embassy-mock-api.service.ts index 515a7c65b..f384cf0eb 100644 --- a/ui/src/app/services/api/embassy/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy/embassy-mock-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core' import { pauseFor } from '../../../util/misc.util' import { ApiService } from './embassy-api.service' -import { PatchOp } from 'patch-db-client' +import { Operation, PatchOp } from 'patch-db-client' import { PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model' import { RR, WithRevision } from '../api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' @@ -12,7 +12,7 @@ import { ConfigService } from '../../config.service' @Injectable() export class MockApiService extends ApiService { - welcomeAck = false + private readonly revertTime = 4000 constructor ( private readonly http: HttpService, @@ -32,7 +32,7 @@ export class MockApiService extends ApiService { // ...Mock.DbDump, // id: this.nextSequence(), // } - return this.http.rpcRequest({ method: 'db.revisions', params: { since } }) + return this.http.rpcRequest({ method: 'db.revisions', params: { since } }) } async getDump (): Promise { @@ -41,7 +41,7 @@ export class MockApiService extends ApiService { // ...Mock.DbDump, // id: this.nextSequence(), // } - return this.http.rpcRequest({ method: 'db.dump' }) + return this.http.rpcRequest({ method: 'db.dump' }) } async setDbValueRaw (params: RR.SetDBValueReq): Promise { @@ -60,7 +60,7 @@ export class MockApiService extends ApiService { // expireId: null, // }, // } - return this.http.rpcRequest({ method: 'db.put.ui', params }) + return this.http.rpcRequest>({ method: 'db.put.ui', params }) } // auth @@ -94,14 +94,41 @@ export class MockApiService extends ApiService { async updateServerRaw (params: RR.UpdateServerReq): Promise { await pauseFor(2000) + const path = '/server-info/status' const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/status', + path, value: ServerStatus.Updating, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + setTimeout(() => { + const patch = [ + { + op: PatchOp.REPLACE, + path, + value: ServerStatus.Running, + }, + { + op: PatchOp.REPLACE, + path: '/server-info/version', + value: this.config.version + '.1', + }, + ] + this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + // quickly revert patch to proper version to prevent infinite refresh loop + const patch2 = [ + { + op: PatchOp.REPLACE, + path: '/server-info/version', + value: this.config.version, + }, + ] + this.http.rpcRequest>({ method: 'db.patch', params: { patch: patch2 } }) + }, this.revertTime) + + return res } async restartServer (params: RR.RestartServerReq): Promise { @@ -135,7 +162,7 @@ export class MockApiService extends ApiService { value: params.url, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) } // async setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise { @@ -147,14 +174,14 @@ export class MockApiService extends ApiService { // value: params.url, // }, // ] - // return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + // return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) // } // password - async updatePassword (params: RR.UpdatePasswordReq): Promise { - await pauseFor(2000) - return null - } + // async updatePassword (params: RR.UpdatePasswordReq): Promise { + // await pauseFor(2000) + // return null + // } // notification @@ -167,7 +194,7 @@ export class MockApiService extends ApiService { value: 0, }, ] - const { revision } = await this.http.rpcRequest({ method: 'db.patch', params: { patch } }) as WithRevision + const { revision } = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) as WithRevision return { response: Mock.Notifications, revision, @@ -200,24 +227,28 @@ export class MockApiService extends ApiService { value: params.ssid, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) } async deleteWifiRaw (params: RR.DeleteWifiReq): Promise { await pauseFor(2000) - const patch = [ + const patch: Operation[] = [ { - op: PatchOp.REPLACE, - path: '/server-info/wifi/selected', - value: null, - }, - { - op: PatchOp.REPLACE, - path: '/server-info/wifi/connected', - value: null, + op: PatchOp.REMOVE, + path: `/server-info/wifi/ssids/${params.ssid}`, }, + // { + // op: PatchOp.REPLACE, + // path: '/server-info/wifi/selected', + // value: null, + // }, + // { + // op: PatchOp.REPLACE, + // path: '/server-info/wifi/connected', + // value: null, + // }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) } // ssh @@ -241,14 +272,26 @@ export class MockApiService extends ApiService { async createBackupRaw (params: RR.CreateBackupReq): Promise { await pauseFor(2000) + const path = '/server-info/status' const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/status', + path, value: ServerStatus.BackingUp, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + setTimeout(() => { + const patch = [ + { + op: PatchOp.REPLACE, + path, + value: ServerStatus.Running, + }, + ] + this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + }, this.revertTime) + return res } async restoreBackupRaw (params: RR.RestoreBackupReq): Promise { @@ -302,7 +345,7 @@ export class MockApiService extends ApiService { value: pkg, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) } async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise { @@ -328,25 +371,32 @@ export class MockApiService extends ApiService { path: `/package-data/${params.id}/installed/status/configured`, value: true, }, - { - op: PatchOp.REPLACE, - path: `/package-data/${params.id}/installed/status/main/status`, - value: PackageMainStatus.Running, - }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) } async restorePackageRaw (params: RR.RestorePackageReq): Promise { await pauseFor(2000) + const path = `/package-data/${params.id}/installed/status/main/status` const patch = [ { op: PatchOp.REPLACE, - path: `/package-data/${params.id}/installed/status/main/status`, + path, value: PackageMainStatus.Restoring, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + const res = await this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + setTimeout(() => { + const patch = [ + { + op: PatchOp.REPLACE, + path, + value: PackageMainStatus.Stopped, + }, + ] + this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) + }, this.revertTime) + return res } async executePackageAction (params: RR.ExecutePackageActionReq): Promise { @@ -361,14 +411,15 @@ export class MockApiService extends ApiService { async startPackageRaw (params: RR.StartPackageReq): Promise { await pauseFor(2000) + const path = `/package-data/${params.id}/installed/status/main/status` const patch = [ { op: PatchOp.REPLACE, - path: `/package-data/${params.id}/installed/status/main/status`, + path, value: PackageMainStatus.Running, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + return this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) } async dryStopPackage (params: RR.DryStopPackageReq): Promise { @@ -378,10 +429,11 @@ export class MockApiService extends ApiService { async stopPackageRaw (params: RR.StopPackageReq): Promise { await pauseFor(2000) + const path = `/package-data/${params.id}/installed/status/main/status` const patch = [ { op: PatchOp.REPLACE, - path: `/package-data/${params.id}/installed/status/main/status`, + path, value: PackageMainStatus.Stopping, }, ] @@ -390,12 +442,12 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: `/package-data/${params.id}/installed/status/main/status`, + path, value: PackageMainStatus.Stopped, }, ] this.http.rpcRequest>({ method: 'db.patch', params: { patch } }) - }, 2000) + }, this.revertTime) return res } @@ -414,7 +466,17 @@ export class MockApiService extends ApiService { value: PackageState.Removing, }, ] - return this.http.rpcRequest({ method: 'db.patch', params: { patch } }) + const 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 } async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise { diff --git a/ui/src/app/services/api/marketplace/marketplace-live-api.service.ts b/ui/src/app/services/api/marketplace/marketplace-live-api.service.ts index 51a0bf128..3be443a2b 100644 --- a/ui/src/app/services/api/marketplace/marketplace-live-api.service.ts +++ b/ui/src/app/services/api/marketplace/marketplace-live-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { HttpService } from '../../http.service' +import { HttpService, Method } from '../../http.service' import { RR } from '../api.types' import { MarketplaceApiService } from './marketplace-api.service' import { PatchDbService } from '../../patch-db/patch-db.service' @@ -17,29 +17,55 @@ export class MarketplaceLiveApiService extends MarketplaceApiService { } async getEos (params: RR.GetMarketplaceEOSReq): Promise { - return this.http.simpleGet(this.getMarketplaceURL('eos'), params) + const url = this.getMarketplaceURL('eos') + return this.http.httpRequest({ + method: Method.GET, + url: url + '/eos', + params, + withCredentials: false, + }) } async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise { - return this.http.simpleGet(this.getMarketplaceURL('package'), params) + const url = this.getMarketplaceURL('package') + return this.http.httpRequest({ + method: Method.GET, + url: url + '/data', + params, + withCredentials: false, + }) } async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise { const url = this.getMarketplaceURL('package', params.ids?.length > 1) - const threadParams = { - ...params, - ids: JSON.stringify(params.ids), - } - - return this.http.simpleGet(url, threadParams) + return this.http.httpRequest({ + method: Method.GET, + url: url + '/packages', + params: { + ...params, + ids: JSON.stringify(params.ids), + }, + withCredentials: false, + }) } async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise { - return this.http.simpleGet(this.getMarketplaceURL('package'), params) + const url = this.getMarketplaceURL('package') + return this.http.httpRequest({ + method: Method.GET, + url: url + + '/release-notes', + params, + withCredentials: false, + }) } async getLatestVersion (params: RR.GetLatestVersionReq): Promise { const url = this.getMarketplaceURL('package', params.ids?.length > 1) - return this.http.simpleGet(url, params) + return this.http.httpRequest({ + method: Method.GET, + url: url + '/latest-version', + params, + withCredentials: false, + }) } } diff --git a/ui/src/app/services/api/marketplace/marketplace-mock-api.service.ts b/ui/src/app/services/api/marketplace/marketplace-mock-api.service.ts index bacbc1ee5..a945b8ac0 100644 --- a/ui/src/app/services/api/marketplace/marketplace-mock-api.service.ts +++ b/ui/src/app/services/api/marketplace/marketplace-mock-api.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core' import { pauseFor } from '../../../util/misc.util' import { RR } from '../api.types' import { Mock } from '../api.fixures' -import { HttpService } from '../../http.service' +import { HttpService, Method } from '../../http.service' import { MarketplaceApiService } from './marketplace-api.service' import { PatchDbService } from '../../patch-db/patch-db.service' import { ConfigService } from '../../config.service' @@ -20,53 +20,68 @@ export class MarketplaceMockApiService extends MarketplaceApiService { // marketplace async getEos (params: RR.GetMarketplaceEOSReq): Promise { - let url = this.getMarketplaceURL('eos') + const url = this.getMarketplaceURL('eos') if (this.useLocal(url)) { await pauseFor(2000) return Mock.MarketplaceEos } - url = `${url}/sys/version/eos` - return this.http.simpleGet(url) + return this.http.httpRequest({ + method: Method.GET, + url: `${url}/eos`, + params, + withCredentials: false, + }) } async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise { - let url = this.getMarketplaceURL('package') + const url = this.getMarketplaceURL('package') if (this.useLocal(url)) { await pauseFor(2000) return { categories: ['featured', 'bitcoin', 'lightning', 'data', 'messaging', 'social', 'alt coin'], } } - url = `${url}/data` - return this.http.simpleGet(url) + return this.http.httpRequest({ + method: Method.GET, + url: `${url}/data`, + params, + withCredentials: false, + }) } async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise { - let url = this.getMarketplaceURL('package', params.ids?.length > 1) - const threadParams = { - ...params, - ids: JSON.stringify(params.ids), - } + const url = this.getMarketplaceURL('package', params.ids?.length > 1) if (this.useLocal(url)) { await pauseFor(2000) - return Mock.AvailableList + return Mock.MarketplacePkgsList } - url = `${url}/packages` - return this.http.simpleGet(url, threadParams) + return this.http.httpRequest({ + method: Method.GET, + url: `${url}/packages`, + params: { + ...params, + ids: JSON.stringify(params.ids), + }, + withCredentials: false, + }) } async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise { - let url = this.getMarketplaceURL('package') + const url = this.getMarketplaceURL('package') if (this.useLocal(url)) { await pauseFor(2000) return Mock.ReleaseNotes } - url = `${url}/release-notes` - return this.http.simpleGet(url, params) + return this.http.httpRequest({ + method: Method.GET, + url: `${url}/release-notes`, + params, + withCredentials: false, + }) } async getLatestVersion (params: RR.GetLatestVersionReq): Promise { - let url = this.getMarketplaceURL('package', params.ids?.length > 1) + const url = this.getMarketplaceURL('package', params.ids?.length > 1) if (this.useLocal(url)) { await pauseFor(2000) return params.ids.reduce((obj, id) => { @@ -74,8 +89,13 @@ export class MarketplaceMockApiService extends MarketplaceApiService { return obj }, { }) } - url = `${url}/latest-version` - return this.http.simpleGet(url) + + return this.http.httpRequest({ + method: Method.GET, + url: `${url}/latest-version`, + params, + withCredentials: false, + }) } private useLocal (url: string): boolean { diff --git a/ui/src/app/services/connection.service.ts b/ui/src/app/services/connection.service.ts index c8e20a160..351e0a2ff 100644 --- a/ui/src/app/services/connection.service.ts +++ b/ui/src/app/services/connection.service.ts @@ -32,7 +32,7 @@ export class ConnectionService { combineLatest([this.networkState$.pipe(distinctUntilChanged()), this.patch.watchConnection$().pipe(distinctUntilChanged())]) .subscribe(async ([network, connectionStatus]) => { - const addrs = this.patch.data['server-info']?.['connection-addresses'] + const addrs = this.patch.data['server-info']['connection-addresses'] if (connectionStatus !== ConnectionStatus.Disconnected) { this.connectionFailure$.next(ConnectionFailure.None) } else if (!network) { @@ -42,12 +42,12 @@ export class ConnectionService { } else { // diagnosing this.connectionFailure$.next(ConnectionFailure.Diagnosing) - const torSuccess = await this.testAddrs(addrs?.tor || []) + const torSuccess = await this.testAddrs(addrs.tor) if (torSuccess) { // TOR SUCCESS, EMBASSY IS PROBLEM this.connectionFailure$.next(ConnectionFailure.Embassy) } else { - const clearnetSuccess = await this.testAddrs(addrs?.clearnet || []) + const clearnetSuccess = await this.testAddrs(addrs.clearnet) if (clearnetSuccess) { // CLEARNET SUCCESS, TOR IS PROBLEM this.connectionFailure$.next(ConnectionFailure.Tor) @@ -76,6 +76,7 @@ export class ConnectionService { await this.httpService.httpRequest({ method: Method.GET, url: addr, + withCredentials: false, }) return true } catch (e) { diff --git a/ui/src/app/services/http.service.ts b/ui/src/app/services/http.service.ts index b6a5c203f..e7c554b99 100644 --- a/ui/src/app/services/http.service.ts +++ b/ui/src/app/services/http.service.ts @@ -10,7 +10,6 @@ import { Revision } from 'patch-db-client' }) export class HttpService { private unauthorizedApiResponse$ = new Subject() - authReqEnabled: boolean = false fullUrl: string constructor ( @@ -44,22 +43,15 @@ export class HttpService { if (isRpcSuccess(res)) return res.result } - async simpleGet (url: string, params: { [param: string]: string | string[] } = { }): Promise { - Object.keys(params).forEach(key => { - if (!params[key]) delete params[key] - }) - return this.http.get(url, { params }).toPromise() - } - async httpRequest (httpOpts: HttpOptions): Promise { - let { body, timeout, ...rest} = this.translateOptions(httpOpts) + let { body, timeout, url, ...rest} = this.translateOptions(httpOpts) let req: Observable<{ body: T }> switch (httpOpts.method){ - case Method.GET: req = this.http.get(this.fullUrl, rest) as any; break - case Method.POST: req = this.http.post(this.fullUrl, body, rest) as any; break - case Method.PUT: req = this.http.put(this.fullUrl, body, rest) as any; break - case Method.PATCH: req = this.http.patch(this.fullUrl, body, rest) as any; break - case Method.DELETE: req = this.http.delete(this.fullUrl, rest) as any; break + case Method.GET: req = this.http.get(url, rest) as any; break + case Method.POST: req = this.http.post(url, body, rest) as any; break + case Method.PUT: req = this.http.put(url, body, rest) as any; break + case Method.PATCH: req = this.http.patch(url, body, rest) as any; break + case Method.DELETE: req = this.http.delete(url, rest) as any; break } return (timeout ? withTimeout(req, timeout) : req) @@ -68,16 +60,25 @@ export class HttpService { .catch(e => { throw new HttpError(e) }) } - translateOptions (httpOpts: HttpOptions): HttpJsonOptions { + private translateOptions (httpOpts: HttpOptions): HttpJsonOptions { + if (httpOpts.withCredentials !== false) { + httpOpts.withCredentials = this.config.mocks.enabled ? false : true + } + + const urlIsRelative = !httpOpts.url || httpOpts.url.startsWith('/') + const url = urlIsRelative ? + this.fullUrl + httpOpts.url : + httpOpts.url + return { observe: 'events', responseType: 'json', reportProgress: false, - withCredentials: this.config.mocks.enabled ? false : true, + withCredentials: httpOpts.withCredentials, headers: httpOpts.headers, params: httpOpts.params, body: httpOpts.data || { }, - url: httpOpts.url, + url, timeout: httpOpts.readTimeout, } } diff --git a/ui/src/app/services/patch-db/patch-db.service.ts b/ui/src/app/services/patch-db/patch-db.service.ts index 1460debdc..a703ee08d 100644 --- a/ui/src/app/services/patch-db/patch-db.service.ts +++ b/ui/src/app/services/patch-db/patch-db.service.ts @@ -20,7 +20,6 @@ export enum ConnectionStatus { }) export class PatchDbService { connectionStatus$ = new BehaviorSubject(ConnectionStatus.Initializing) - sequence$: Observable data: DataModel private patchDb: PatchDB private patchSub: Subscription @@ -33,10 +32,7 @@ export class PatchDbService { async init (): Promise { const cache = await this.bootstrapper.init() - console.log('CACHECACHE', cache) this.patchDb = new PatchDB([this.source, this.http], this.http, cache) - - this.sequence$ = this.patchDb.store.sequence$.asObservable() this.data = this.patchDb.store.cache.data } diff --git a/ui/src/app/services/startup-alerts.service.ts b/ui/src/app/services/startup-alerts.service.ts index 6f8813ace..9e9128103 100644 --- a/ui/src/app/services/startup-alerts.service.ts +++ b/ui/src/app/services/startup-alerts.service.ts @@ -28,8 +28,8 @@ export class StartupAlertsService { private readonly wizardBaker: WizardBaker, private readonly patch: PatchDbService, ) { - const welcome: Check = { - name: 'welcome', + const osWelcome: Check = { + name: 'osWelcome', shouldRun: () => this.shouldRunOsWelcome(), check: async () => true, display: () => this.displayOsWelcome(), @@ -42,14 +42,14 @@ export class StartupAlertsService { display: pkg => this.displayOsUpdateCheck(pkg), hasRun: this.config.skipStartupAlerts, } - const apps: Check = { - name: 'apps', + const pkgsUpdate: Check = { + name: 'pkgsUpdate', shouldRun: () => this.shouldRunAppsCheck(), check: () => this.appsCheck(), display: () => this.displayAppsCheck(), hasRun: this.config.skipStartupAlerts, } - this.checks = [welcome, osUpdate, apps] + this.checks = [osWelcome, osUpdate, pkgsUpdate] } // This takes our three checks and filters down to those that should run. @@ -79,8 +79,7 @@ export class StartupAlertsService { } private shouldRunOsWelcome (): boolean { - const data = this.patch.data - return !data.ui['welcome-ack'] && data['server-info'].version === this.config.version + return this.patch.data.ui['welcome-ack'] !== this.config.version } private shouldRunOsUpdateCheck (): boolean { @@ -94,7 +93,7 @@ export class StartupAlertsService { private async osUpdateCheck (): Promise { const res = await this.marketplaceApi.getEos({ }) - if (this.emver.compare(this.patch.data['server-info'].version, res.version) === -1) { + if (this.emver.compare(this.config.version, res.version) === -1) { return res } else { return undefined @@ -113,7 +112,7 @@ export class StartupAlertsService { component: OSWelcomePage, presentingElement: await this.modalCtrl.getTop(), componentProps: { - version: this.patch.data['server-info'].version, + version: this.config.version, }, })