global click protections, install wizard fixes, better login, better marketplace search, fix mocks

This commit is contained in:
Matt Hill
2021-08-31 16:35:25 -06:00
committed by Aiden McClelland
parent 14a0dbe66e
commit 7b9ce88a16
18 changed files with 266 additions and 212 deletions

View File

@@ -14,7 +14,7 @@ import { ServerStatus } from './services/patch-db/data-model'
import { ConnectionFailure, ConnectionService } from './services/connection.service' import { ConnectionFailure, ConnectionService } from './services/connection.service'
import { StartupAlertsService } from './services/startup-alerts.service' import { StartupAlertsService } from './services/startup-alerts.service'
import { ConfigService } from './services/config.service' import { ConfigService } from './services/config.service'
import { isEmptyObject } from './util/misc.util' import { debounce, isEmptyObject } from './util/misc.util'
import { ErrorToastService } from './services/error-toast.service' import { ErrorToastService } from './services/error-toast.service'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
@@ -24,15 +24,13 @@ import { Subscription } from 'rxjs'
styleUrls: ['app.component.scss'], styleUrls: ['app.component.scss'],
}) })
export class AppComponent { export class AppComponent {
@HostListener('document:keypress', ['$event']) @HostListener('document:keydown.enter', ['$event'])
handleKeyboardEvent (event: KeyboardEvent) { @debounce()
if (event.key === 'Enter') { handleKeyboardEvent () {
const elems = document.getElementsByClassName('enter-click')
const elems = document.getElementsByClassName('enter-click') const elem = elems[elems.length - 1] as HTMLButtonElement
const elem = elems[elems.length - 1] as HTMLButtonElement if (!elem || elem.classList.contains('no-click')) return
if (!elem || elem.classList.contains('no-click')) return if (elem) elem.click()
if (elem) elem.click()
}
} }
ServerStatus = ServerStatus ServerStatus = ServerStatus

View File

@@ -49,12 +49,12 @@
<!-- next/finish buttons --> <!-- next/finish buttons -->
<ng-container *ngIf="!(currentSlide.loading$ | async)"> <ng-container *ngIf="!(currentSlide.loading$ | async)">
<!-- next --> <!-- next -->
<ion-button slot="end" *ngIf="currentBottomBar.next as next" (click)="transitions.next()" fill="outline" class="toolbar-button enter-click"> <ion-button slot="end" *ngIf="currentBottomBar.next as next" (click)="callTransition(transitions.next)" fill="outline" class="toolbar-button enter-click" [class.no-click]="transitioning">
<ion-text [class.smaller-text]="next.length > 16">{{ next }}</ion-text> <ion-text [class.smaller-text]="next.length > 16">{{ next }}</ion-text>
</ion-button> </ion-button>
<!-- finish --> <!-- finish -->
<ion-button slot="end" *ngIf="currentBottomBar.finish as finish" (click)="transitions.final()" fill="outline" class="toolbar-button enter-click"> <ion-button slot="end" *ngIf="currentBottomBar.finish as finish" (click)="callTransition(transitions.final)" fill="outline" class="toolbar-button enter-click" [class.no-click]="transitioning">
<ion-text [class.smaller-text]="finish.length > 16">{{ finish }}</ion-text> <ion-text [class.smaller-text]="finish.length > 16">{{ finish }}</ion-text>
</ion-button> </ion-button>
</ng-container> </ng-container>

View File

@@ -14,6 +14,8 @@ import { WizardAction } from './wizard-types'
styleUrls: ['./install-wizard.component.scss'], styleUrls: ['./install-wizard.component.scss'],
}) })
export class InstallWizardComponent { export class InstallWizardComponent {
transitioning = false
@Input() params: { @Input() params: {
// defines each slide along with bottom bar // defines each slide along with bottom bar
slideDefinitions: SlideDefinition[] slideDefinitions: SlideDefinition[]
@@ -86,6 +88,12 @@ export class InstallWizardComponent {
await this.slideContainer.lockSwipes(true) await this.slideContainer.lockSwipes(true)
}) })
} }
async callTransition (transition: Function) {
this.transitioning = true
await transition()
this.transitioning = false
}
} }
export interface SlideDefinition { export interface SlideDefinition {

View File

@@ -271,9 +271,8 @@ export class WizardBaker {
id: string id: string
title: string title: string
version: string version: string
breakages: Breakages
}): InstallWizardComponent['params'] { }): InstallWizardComponent['params'] {
const { breakages, title, version } = values const { title, version, id } = values
const action = 'stop' const action = 'stop'
const toolbar: TopbarParams = { action, title, version } const toolbar: TopbarParams = { action, title, version }
@@ -286,10 +285,22 @@ export class WizardBaker {
action, action,
verb: 'stopping', verb: 'stopping',
title, title,
fetchBreakages: () => Promise.resolve(breakages), fetchBreakages: () => this.embassyApi.dryStopPackage({ id }).then(breakages => breakages),
}, },
}, },
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Stop Anyways' }, bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Stop Service' },
},
{
slide: {
selector: 'complete',
params: {
action,
verb: 'stopping',
title,
executeAction: () => this.embassyApi.stopPackage({ id }),
},
},
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },
}, },
] ]
return { toolbar, slideDefinitions } return { toolbar, slideDefinitions }
@@ -321,4 +332,4 @@ export class WizardBaker {
} }
} }
const defaultUninstallWarning = serviceName => `Uninstalling ${ serviceName } will result in the deletion of its data.` const defaultUninstallWarning = (serviceName: string) => `Uninstalling ${ serviceName } will result in the deletion of its data.`

View File

@@ -6,9 +6,7 @@
class="ion-padding" class="ion-padding"
> >
<ion-infinite-scroll id="scroller" *ngIf="!loading && needInfinite" position="top" threshold="0" (ionInfinite)="loadData($event)"> <ion-infinite-scroll id="scroller" *ngIf="!loading && needInfinite" position="top" threshold="0" (ionInfinite)="loadData($event)">
<ion-infinite-scroll-content <ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
loadingSpinner="bubbles">
</ion-infinite-scroll-content>
</ion-infinite-scroll> </ion-infinite-scroll>
<text-spinner *ngIf="loading" text="Loading Logs"></text-spinner> <text-spinner *ngIf="loading" text="Loading Logs"></text-spinner>
@@ -28,14 +26,14 @@
[ngStyle]="{ [ngStyle]="{
'position': 'fixed', 'position': 'fixed',
'bottom': '50px', 'bottom': '50px',
'right': isOnBottom ? '-50px' : '30px', 'right': isOnBottom ? '-52px' : '30px',
'background-color': 'var(--ion-color-medium)', 'background-color': 'var(--ion-color-medium)',
'border-radius': '100%', 'border-radius': '100%',
'transition': 'right 0.4s ease-out' 'transition': 'right 0.4s ease-out'
}" }"
> >
<ion-button style="width: 50px; height: 50px; --padding-start: 0px; --padding-end: 0px; --border-radius: 100%;" fill="clear" (click)="scrollToBottom()"> <ion-button style="width: 50px; height: 50px; --padding-start: 0px; --padding-end: 0px; --border-radius: 100%;" color="dark" (click)="scrollToBottom()" strong>
<ion-icon style="font-size: 60px; --ionicon-stroke-width: 8px;" name="chevron-down-circle-outline"></ion-icon> <ion-icon name="chevron-down"></ion-icon>
</ion-button> </ion-button>
</div> </div>

View File

@@ -8,41 +8,48 @@
</ion-header> </ion-header>
<ion-content style="position: relative"> <ion-content style="position: relative">
<div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding">
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh"> <!-- loading -->
<h2>Welcome to <ion-text color="danger" style="font-family: 'Montserrat';">Embassy</ion-text></h2> <text-spinner *ngIf="loading" text="Connecting to Embassy"></text-spinner>
<p class="ion-text-wrap">Get started by installing your first service.</p>
<!-- not loading -->
<div *ngIf="!loading">
<div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding">
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
<h2>Welcome to <ion-text color="danger" style="font-family: 'Montserrat';">Embassy</ion-text></h2>
<p class="ion-text-wrap">Get started by installing your first service.</p>
</div>
<ion-button color="dark" [routerLink]="['/marketplace']" style="width: 50%;">
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>
</div> </div>
<ion-button color="dark" [routerLink]="['/marketplace']" style="width: 50%;">
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>
</div>
<ng-template #list>
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeLg="3" sizeXl="2">
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
<div class="launch-container" *ngIf="pkg.value.entry.manifest.interfaces | hasUi">
<div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry.state | isLaunchable : pkg.value.entry.installed?.status.main.status : pkg.value.entry.manifest.interfaces)">
<ion-icon name="open-outline"></ion-icon>
</div>
</div>
<img style="position: absolute" class="main-img" [src]="pkg.value.entry['static-files'].icon" alt="icon" /> <ng-template #list>
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="> <ion-grid>
<img *ngIf="connectionFailure" class="bulb-off" src="assets/img/off-bulb.png" /> <ion-row>
<img *ngIf="!connectionFailure" [class]="pkg.value.bulb.class" [src]="pkg.value.bulb.img" /> <ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeLg="3" sizeXl="2">
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
<ion-card-header> <div class="launch-container" *ngIf="pkg.value.entry.manifest.interfaces | hasUi">
<status *ngIf="[PackageState.Installed, PackageState.Removing] | includes : pkg.value.entry.state" [disconnected]="connectionFailure" [rendering]="pkg.value.statusRendering" size="calc(8px + .4vw)" weight="bold"></status> <div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry.state | isLaunchable : pkg.value.entry.installed?.status.main.status : pkg.value.entry.manifest.interfaces)">
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.value.entry.state" class="main-status"><ion-text color="primary">{{ pkg.value.entry.state | titlecase }}...{{ (pkg.value.entry['install-progress'] | installState).totalProgress }}%</ion-text></p> <ion-icon name="open-outline"></ion-icon>
<ion-card-title>{{ pkg.value.entry.manifest.title }}</ion-card-title> </div>
</ion-card-header> </div>
</ion-card>
</ion-col> <img style="position: absolute" class="main-img" [src]="pkg.value.entry['static-files'].icon" alt="icon" />
</ion-row> <img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
</ion-grid> <img *ngIf="connectionFailure" class="bulb-off" src="assets/img/off-bulb.png" />
</ng-template> <img *ngIf="!connectionFailure" [class]="pkg.value.bulb.class" [src]="pkg.value.bulb.img" />
<ion-card-header>
<status *ngIf="[PackageState.Installed, PackageState.Removing] | includes : pkg.value.entry.state" [disconnected]="connectionFailure" [rendering]="pkg.value.statusRendering" size="calc(8px + .4vw)" weight="bold"></status>
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.value.entry.state" class="main-status"><ion-text color="primary">{{ pkg.value.entry.state | titlecase }}...{{ (pkg.value.entry['install-progress'] | installState).totalProgress }}%</ion-text></p>
<ion-card-title>{{ pkg.value.entry.manifest.title }}</ion-card-title>
</ion-card-header>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
</div>
</ion-content> </ion-content>

View File

@@ -5,7 +5,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model' import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { filter } from 'rxjs/operators' import { delay, filter } from 'rxjs/operators'
@Component({ @Component({
selector: 'app-list', selector: 'app-list',
@@ -25,6 +25,7 @@ export class AppListPage {
sub: Subscription | null sub: Subscription | null
}} = { } }} = { }
PackageState = PackageState PackageState = PackageState
loading = true
constructor ( constructor (
private readonly config: ConfigService, private readonly config: ConfigService,
@@ -41,6 +42,8 @@ export class AppListPage {
}), }),
) )
.subscribe(pkgs => { .subscribe(pkgs => {
this.loading = false
const ids = Object.keys(pkgs) const ids = Object.keys(pkgs)
Object.keys(this.pkgs).forEach(id => { Object.keys(this.pkgs).forEach(id => {

View File

@@ -84,33 +84,31 @@ export class AppShowPage {
async stop (): Promise<void> { async stop (): Promise<void> {
const { id, title, version } = this.pkg.manifest const { id, title, version } = this.pkg.manifest
const loader = await this.loadingCtrl.create({
message: `Stopping...`,
spinner: 'lines',
cssClass: 'loader',
})
await loader.present()
try { if (isEmptyObject(this.pkg.installed['current-dependents'])) {
const breakages = await this.embassyApi.dryStopPackage({ id }) const loader = await this.loadingCtrl.create({
message: `Stopping...`,
spinner: 'lines',
cssClass: 'loader',
})
await loader.present()
if (!isEmptyObject(breakages)) { try {
const { cancelled } = await wizardModal( await this.embassyApi.stopPackage({ id })
this.modalCtrl, } catch (e) {
this.wizardBaker.stop({ this.errToast.present(e)
id, } finally {
title, loader.dismiss()
version,
breakages,
}),
)
if (cancelled) return
} }
await this.embassyApi.stopPackage({ id }) } else {
} catch (e) { wizardModal(
this.errToast.present(e) this.modalCtrl,
} finally { this.wizardBaker.stop({
loader.dismiss() id,
title,
version,
}),
)
} }
} }

View File

@@ -2,7 +2,6 @@ import { Component } from '@angular/core'
import { LoadingController } from '@ionic/angular' import { LoadingController } from '@ionic/angular'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { AuthService } from 'src/app/services/auth.service' import { AuthService } from 'src/app/services/auth.service'
import { PatchConnection, PatchDbService } from 'src/app/services/patch-db/patch-db.service'
@Component({ @Component({
selector: 'login', selector: 'login',
@@ -19,13 +18,8 @@ export class LoginPage {
constructor ( constructor (
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly patch: PatchDbService,
) { } ) { }
ngOnInit () {
}
ngOnDestroy () { ngOnDestroy () {
if (this.loader) { if (this.loader) {
this.loader.dismiss() this.loader.dismiss()
@@ -52,16 +46,10 @@ export class LoginPage {
try { try {
await this.authService.login(this.password) await this.authService.login(this.password)
this.loader.message = 'Loading Embassy Data'
this.password = '' this.password = ''
this.patchConnectionSub = this.patch.watchPatchConnection$().subscribe(status => {
if (status === PatchConnection.Disconnected) {
this.error = 'Connection failed'
this.loader.dismiss()
}
})
} catch (e) { } catch (e) {
this.error = e.message this.error = e.message
} finally {
this.loader.dismiss() this.loader.dismiss()
} }
} }

View File

@@ -6,6 +6,7 @@ import { MarketplaceListPage } from './marketplace-list.page'
import { SharingModule } from '../../../modules/sharing.module' import { SharingModule } from '../../../modules/sharing.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { FormsModule } from '@angular/forms'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -18,6 +19,7 @@ const routes: Routes = [
imports: [ imports: [
CommonModule, CommonModule,
IonicModule, IonicModule,
FormsModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
StatusComponentModule, StatusComponentModule,
SharingModule, SharingModule,

View File

@@ -9,7 +9,14 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<h1 style="font-family: 'Montserrat'; font-weight: 100px; margin: 32px 0;" class="ion-text-center">Embassy Marketplace</h1> <h1 style="font-family: 'Montserrat'; font-weight: 100px; margin: 32px 0;" class="ion-text-center">Embassy Marketplace</h1>
<ion-searchbar color="dark" (ionChange)="search($event)" debounce="400" style="padding-bottom: 32px;"></ion-searchbar> <ion-toolbar style="padding-bottom: 32px; max-width: 600px;" color="transparent">
<ion-searchbar color="dark" (keyup.enter)="search()" debounce="300" [(ngModel)]="query"></ion-searchbar>
<ion-buttons slot="end">
<ion-button color="primary" fill="solid" (click)="search()">
<ion-icon slot="icon-only" name="search-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
<!-- page loading --> <!-- page loading -->
<ng-container *ngIf="pageLoading; else pageLoaded"> <ng-container *ngIf="pageLoading; else pageLoaded">

View File

@@ -88,10 +88,11 @@ export class MarketplaceListPage {
e.target.complete() e.target.complete()
} }
async search (e?: any): Promise<void> { async search (): Promise<void> {
this.query = e.target.value || undefined if (!this.query) return
this.page = 1
this.pkgsLoading = true this.pkgsLoading = true
this.category = undefined
this.page = 1
await this.getPkgs() await this.getPkgs()
} }
@@ -134,9 +135,9 @@ export class MarketplaceListPage {
} }
async switchCategory (category: string): Promise<void> { async switchCategory (category: string): Promise<void> {
this.pkgs = []
this.category = category
this.pkgsLoading = true this.pkgsLoading = true
this.category = category
this.query = undefined
this.page = 1 this.page = 1
await this.getPkgs() await this.getPkgs()
} }

View File

@@ -581,56 +581,58 @@ export module Mock {
export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = Object.values(Mock.MarketplacePkgs).map(service => service['latest']) export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = Object.values(Mock.MarketplacePkgs).map(service => service['latest'])
export const bitcoinproxy: PackageDataEntry = { export const Pkgs: { [key: string]: PackageDataEntry } = {
state: PackageState.Installed, 'bitcoin-proxy': {
'static-files': { state: PackageState.Installed,
license: 'licenseUrl', // /public/package-data/bitcoinproxy/0.21.1/LICENSE.md, 'static-files': {
icon: 'assets/img/service-icons/bitcoin-proxy.png', license: 'licenseUrl', // /public/package-data/bitcoinproxy/0.21.1/LICENSE.md,
instructions: 'instructionsUrl', // /public/package-data/bitcoinproxy/0.2.2/INSTRUCTIONS.md icon: 'assets/img/service-icons/bitcoin-proxy.png',
}, instructions: 'instructionsUrl', // /public/package-data/bitcoinproxy/0.2.2/INSTRUCTIONS.md
manifest: MockManifestBitcoinProxy,
installed: {
status: {
configured: true,
main: {
status: PackageMainStatus.Running,
started: new Date().toISOString(),
health: { },
},
'dependency-errors': { },
}, },
manifest: MockManifestBitcoinProxy, manifest: MockManifestBitcoinProxy,
'interface-addresses': { installed: {
rpc: { status: {
'tor-address': 'bitcoinproxy-rpc-address.onion', configured: true,
'lan-address': 'bitcoinproxy-rpc-address.local', main: {
}, status: PackageMainStatus.Running,
}, started: new Date().toISOString(),
'system-pointers': [], health: { },
'current-dependents': { },
'lnd': { 'dependency-errors': { },
pointers: [], },
'health-checks': [], manifest: MockManifestBitcoinProxy,
}, 'interface-addresses': {
}, rpc: {
'current-dependencies': { 'tor-address': 'bitcoinproxy-rpc-address.onion',
'bitcoind': { 'lan-address': 'bitcoinproxy-rpc-address.local',
pointers: [], },
'health-checks': [], },
}, 'system-pointers': [],
}, 'current-dependents': {
'dependency-info': { 'lnd': {
'lnd': { pointers: [],
manifest: Mock.MockManifestLnd, 'health-checks': [],
icon: 'assets/img/service-icons/lnd.png', },
}, },
'bitcoind': { 'current-dependencies': {
manifest: Mock.MockManifestBitcoind, 'bitcoind': {
icon: 'assets/img/service-icons/bitcoind.png', pointers: [],
'health-checks': [],
},
},
'dependency-info': {
'lnd': {
manifest: Mock.MockManifestLnd,
icon: 'assets/img/service-icons/lnd.png',
},
'bitcoind': {
manifest: Mock.MockManifestBitcoind,
icon: 'assets/img/service-icons/bitcoind.png',
},
}, },
}, },
'install-progress': undefined,
}, },
'install-progress': undefined,
} }
export const Notifications: ServerNotifications = [ export const Notifications: ServerNotifications = [
@@ -1455,62 +1457,62 @@ export module Mock {
// 'install-progress': undefined, // 'install-progress': undefined,
// } // }
export const lnd: PackageDataEntry = { // export const lnd: PackageDataEntry = {
state: PackageState.Installed, // state: PackageState.Installed,
'static-files': { // 'static-files': {
license: 'licenseUrl', // /public/package-data/lnd/0.21.1/LICENSE.md, // license: 'licenseUrl', // /public/package-data/lnd/0.21.1/LICENSE.md,
icon: 'assets/img/service-icons/lnd.png', // icon: 'assets/img/service-icons/lnd.png',
instructions: 'instructionsUrl', // /public/package-data/lnd/0.21.1/INSTRUCTIONS.md // instructions: 'instructionsUrl', // /public/package-data/lnd/0.21.1/INSTRUCTIONS.md
}, // },
manifest: MockManifestLnd, // manifest: MockManifestLnd,
installed: { // installed: {
status: { // status: {
configured: true, // configured: true,
main: { // main: {
status: PackageMainStatus.Stopped, // status: PackageMainStatus.Stopped,
}, // },
'dependency-errors': { // 'dependency-errors': {
'bitcoin-proxy': { // 'bitcoin-proxy': {
type: DependencyErrorType.NotInstalled, // type: DependencyErrorType.NotInstalled,
}, // },
}, // },
}, // },
manifest: MockManifestLnd, // manifest: MockManifestLnd,
'interface-addresses': { // 'interface-addresses': {
rpc: { // rpc: {
'tor-address': 'lnd-rpc-address.onion', // 'tor-address': 'lnd-rpc-address.onion',
'lan-address': 'lnd-rpc-address.local', // 'lan-address': 'lnd-rpc-address.local',
}, // },
grpc: { // grpc: {
'tor-address': 'lnd-grpc-address.onion', // 'tor-address': 'lnd-grpc-address.onion',
'lan-address': 'lnd-grpc-address.local', // 'lan-address': 'lnd-grpc-address.local',
}, // },
}, // },
'system-pointers': [], // 'system-pointers': [],
'current-dependents': { }, // 'current-dependents': { },
'current-dependencies': { // 'current-dependencies': {
'bitcoind': { // 'bitcoind': {
pointers: [], // pointers: [],
'health-checks': [], // 'health-checks': [],
}, // },
'bitcoin-proxy': { // 'bitcoin-proxy': {
pointers: [], // pointers: [],
'health-checks': [], // 'health-checks': [],
}, // },
}, // },
'dependency-info': { // 'dependency-info': {
'bitcoind': { // 'bitcoind': {
manifest: Mock.MockManifestBitcoind, // manifest: Mock.MockManifestBitcoind,
icon: 'assets/img/service-icons/bitcoind.png', // icon: 'assets/img/service-icons/bitcoind.png',
}, // },
'bitcoin-proxy': { // 'bitcoin-proxy': {
manifest: Mock.MockManifestBitcoinProxy, // manifest: Mock.MockManifestBitcoinProxy,
icon: 'assets/img/service-icons/bitcoin-proxy.png', // icon: 'assets/img/service-icons/bitcoin-proxy.png',
}, // },
}, // },
}, // },
'install-progress': undefined, // 'install-progress': undefined,
} // }
// export const DbDump: RR.GetDumpRes = { // export const DbDump: RR.GetDumpRes = {
// id: 1, // id: 1,

View File

@@ -13,6 +13,8 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
return this.sync.asObservable() return this.sync.asObservable()
} }
connectionMade$ = new Subject<void>()
// for getting static files: ex icons, instructions, licenses // for getting static files: ex icons, instructions, licenses
abstract getStatic (url: string): Promise<string> abstract getStatic (url: string): Promise<string>
@@ -195,6 +197,7 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
throw e throw e
}) })
.then(({ response, revision }) => { .then(({ response, revision }) => {
this.connectionMade$.next()
if (revision) this.sync.next(revision) if (revision) this.sync.next(revision)
return response return response
}) })

View File

@@ -96,6 +96,7 @@ export class LiveApiService extends ApiService {
} }
async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise <RR.GetMarketplacePackagesRes> { async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise <RR.GetMarketplacePackagesRes> {
if (params.query) params.category = undefined
return this.http.httpRequest({ return this.http.httpRequest({
method: Method.GET, method: Method.GET,
url: '/marketplace/package/index', url: '/marketplace/package/index',

View File

@@ -335,7 +335,7 @@ export class MockApiService extends ApiService {
'unpack-complete': false, 'unpack-complete': false,
} }
const pkg: PackageDataEntry = { const pkg: PackageDataEntry = {
...Mock[params.id], ...Mock.Pkgs[params.id],
state: PackageState.Installing, state: PackageState.Installing,
'install-progress': initialProgress, 'install-progress': initialProgress,
} }

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, InjectionToken } from '@angular/core' import { Inject, Injectable, InjectionToken } from '@angular/core'
import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client' import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client'
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs' import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs'
import { catchError, debounceTime, finalize, map, tap } from 'rxjs/operators' import { catchError, debounceTime, delay, filter, finalize, map, take, tap, timeout } from 'rxjs/operators'
import { pauseFor } from 'src/app/util/misc.util' import { pauseFor } from 'src/app/util/misc.util'
import { ApiService } from '../api/embassy-api.service' import { ApiService } from '../api/embassy-api.service'
import { DataModel } from './data-model' import { DataModel } from './data-model'
@@ -42,18 +42,30 @@ export class PatchDbService {
start (): void { start (): void {
// make sure everything is stopped before initializing // make sure everything is stopped before initializing
if (this.patchSub) { if (this.patchSub) {
console.log('Retrying')
this.patchSub.unsubscribe() this.patchSub.unsubscribe()
this.patchSub = undefined this.patchSub = undefined
} }
console.log('Retrying')
try { try {
this.patchSub = this.patchDb.sync$() const connectedSub$ = this.patchDb.connectionMade$()
.pipe(debounceTime(500)) .pipe(
.subscribe({ tap(() => {
next: cache => {
this.patchConnection$.next(PatchConnection.Connected) this.patchConnection$.next(PatchConnection.Connected)
}),
timeout(30000),
take(1),
)
const updateSub$ = this.patchDb.sync$()
.pipe(
debounceTime(500),
tap(cache => {
this.bootstrapper.update(cache) this.bootstrapper.update(cache)
}, }),
)
this.patchSub = combineLatest([connectedSub$, updateSub$])
.subscribe({
error: async e => { error: async e => {
console.error('patch-db-sync sub ERROR', e) console.error('patch-db-sync sub ERROR', e)
this.patchConnection$.next(PatchConnection.Disconnected) this.patchConnection$.next(PatchConnection.Disconnected)

View File

@@ -171,4 +171,19 @@ export const exists = (t: any) => {
export type DeepPartial<T> = { export type DeepPartial<T> = {
[k in keyof T]?: DeepPartial<T[k]> [k in keyof T]?: DeepPartial<T[k]>
}
export function debounce (delay: number = 300): MethodDecorator {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const timeoutKey = Symbol()
const original = descriptor.value
descriptor.value = function (...args) {
clearTimeout(this[timeoutKey])
this[timeoutKey] = setTimeout(() => original.apply(this, args), delay)
}
return descriptor
}
} }