From 7b9ce88a16322cff2b8f21d04cd6a0269aae2d39 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 31 Aug 2021 16:35:25 -0600 Subject: [PATCH] global click protections, install wizard fixes, better login, better marketplace search, fix mocks --- ui/src/app/app.component.ts | 18 +- .../install-wizard.component.html | 4 +- .../install-wizard.component.ts | 8 + .../install-wizard/prebaked-wizards.ts | 21 +- ui/src/app/components/logs/logs.page.html | 10 +- .../apps-routes/app-list/app-list.page.html | 77 ++++--- .../apps-routes/app-list/app-list.page.ts | 5 +- .../apps-routes/app-show/app-show.page.ts | 46 ++-- ui/src/app/pages/login/login.page.ts | 14 +- .../marketplace-list.module.ts | 2 + .../marketplace-list.page.html | 9 +- .../marketplace-list/marketplace-list.page.ts | 11 +- ui/src/app/services/api/api.fixures.ts | 204 +++++++++--------- .../app/services/api/embassy-api.service.ts | 3 + .../services/api/embassy-live-api.service.ts | 1 + .../services/api/embassy-mock-api.service.ts | 2 +- .../app/services/patch-db/patch-db.service.ts | 28 ++- ui/src/app/util/misc.util.ts | 15 ++ 18 files changed, 266 insertions(+), 212 deletions(-) diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 6cf629a3c..9b2c6bf2a 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -14,7 +14,7 @@ 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' +import { debounce, isEmptyObject } from './util/misc.util' import { ErrorToastService } from './services/error-toast.service' import { Subscription } from 'rxjs' @@ -24,15 +24,13 @@ import { Subscription } from 'rxjs' styleUrls: ['app.component.scss'], }) export class AppComponent { - @HostListener('document:keypress', ['$event']) - handleKeyboardEvent (event: KeyboardEvent) { - if (event.key === 'Enter') { - - const elems = document.getElementsByClassName('enter-click') - const elem = elems[elems.length - 1] as HTMLButtonElement - if (!elem || elem.classList.contains('no-click')) return - if (elem) elem.click() - } + @HostListener('document:keydown.enter', ['$event']) + @debounce() + handleKeyboardEvent () { + const elems = document.getElementsByClassName('enter-click') + const elem = elems[elems.length - 1] as HTMLButtonElement + if (!elem || elem.classList.contains('no-click')) return + if (elem) elem.click() } ServerStatus = ServerStatus diff --git a/ui/src/app/components/install-wizard/install-wizard.component.html b/ui/src/app/components/install-wizard/install-wizard.component.html index 4ab09245b..e34ec4a2d 100644 --- a/ui/src/app/components/install-wizard/install-wizard.component.html +++ b/ui/src/app/components/install-wizard/install-wizard.component.html @@ -49,12 +49,12 @@ - + {{ next }} - + {{ finish }} diff --git a/ui/src/app/components/install-wizard/install-wizard.component.ts b/ui/src/app/components/install-wizard/install-wizard.component.ts index 92a052e06..fb85d9033 100644 --- a/ui/src/app/components/install-wizard/install-wizard.component.ts +++ b/ui/src/app/components/install-wizard/install-wizard.component.ts @@ -14,6 +14,8 @@ import { WizardAction } from './wizard-types' styleUrls: ['./install-wizard.component.scss'], }) export class InstallWizardComponent { + transitioning = false + @Input() params: { // defines each slide along with bottom bar slideDefinitions: SlideDefinition[] @@ -86,6 +88,12 @@ export class InstallWizardComponent { await this.slideContainer.lockSwipes(true) }) } + + async callTransition (transition: Function) { + this.transitioning = true + await transition() + this.transitioning = false + } } export interface SlideDefinition { diff --git a/ui/src/app/components/install-wizard/prebaked-wizards.ts b/ui/src/app/components/install-wizard/prebaked-wizards.ts index f61cc6daf..5fe57fddd 100644 --- a/ui/src/app/components/install-wizard/prebaked-wizards.ts +++ b/ui/src/app/components/install-wizard/prebaked-wizards.ts @@ -271,9 +271,8 @@ export class WizardBaker { id: string title: string version: string - breakages: Breakages }): InstallWizardComponent['params'] { - const { breakages, title, version } = values + const { title, version, id } = values const action = 'stop' const toolbar: TopbarParams = { action, title, version } @@ -286,10 +285,22 @@ export class WizardBaker { action, verb: 'stopping', 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 } @@ -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.` diff --git a/ui/src/app/components/logs/logs.page.html b/ui/src/app/components/logs/logs.page.html index 640826f1a..db594a434 100644 --- a/ui/src/app/components/logs/logs.page.html +++ b/ui/src/app/components/logs/logs.page.html @@ -6,9 +6,7 @@ class="ion-padding" > - - + @@ -28,14 +26,14 @@ [ngStyle]="{ 'position': 'fixed', 'bottom': '50px', - 'right': isOnBottom ? '-50px' : '30px', + 'right': isOnBottom ? '-52px' : '30px', 'background-color': 'var(--ion-color-medium)', 'border-radius': '100%', 'transition': 'right 0.4s ease-out' }" > - - + + diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/ui/src/app/pages/apps-routes/app-list/app-list.page.html index c1215b267..d70ab3607 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -8,41 +8,48 @@ -
-
-

Welcome to Embassy

-

Get started by installing your first service.

+ + + + + +
+
+
+

Welcome to Embassy

+

Get started by installing your first service.

+
+ + + Marketplace +
- - - Marketplace - -
- - - - - - -
-
- -
-
- icon - - - - - - -

{{ pkg.value.entry.state | titlecase }}...{{ (pkg.value.entry['install-progress'] | installState).totalProgress }}%

- {{ pkg.value.entry.manifest.title }} -
-
-
-
-
-
+ + + + + +
+
+ +
+
+ + icon + + + + + + +

{{ pkg.value.entry.state | titlecase }}...{{ (pkg.value.entry['install-progress'] | installState).totalProgress }}%

+ {{ pkg.value.entry.manifest.title }} +
+
+
+
+
+
+
diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index b8052a8a3..764d593d0 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -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 { Subscription } from 'rxjs' import { PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' -import { filter } from 'rxjs/operators' +import { delay, filter } from 'rxjs/operators' @Component({ selector: 'app-list', @@ -25,6 +25,7 @@ export class AppListPage { sub: Subscription | null }} = { } PackageState = PackageState + loading = true constructor ( private readonly config: ConfigService, @@ -41,6 +42,8 @@ export class AppListPage { }), ) .subscribe(pkgs => { + this.loading = false + const ids = Object.keys(pkgs) Object.keys(this.pkgs).forEach(id => { diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 9e05ee079..dad860329 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -84,33 +84,31 @@ export class AppShowPage { async stop (): Promise { const { id, title, version } = this.pkg.manifest - const loader = await this.loadingCtrl.create({ - message: `Stopping...`, - spinner: 'lines', - cssClass: 'loader', - }) - await loader.present() - try { - const breakages = await this.embassyApi.dryStopPackage({ id }) + if (isEmptyObject(this.pkg.installed['current-dependents'])) { + const loader = await this.loadingCtrl.create({ + message: `Stopping...`, + spinner: 'lines', + cssClass: 'loader', + }) + await loader.present() - if (!isEmptyObject(breakages)) { - const { cancelled } = await wizardModal( - this.modalCtrl, - this.wizardBaker.stop({ - id, - title, - version, - breakages, - }), - ) - if (cancelled) return + try { + await this.embassyApi.stopPackage({ id }) + } catch (e) { + this.errToast.present(e) + } finally { + loader.dismiss() } - await this.embassyApi.stopPackage({ id }) - } catch (e) { - this.errToast.present(e) - } finally { - loader.dismiss() + } else { + wizardModal( + this.modalCtrl, + this.wizardBaker.stop({ + id, + title, + version, + }), + ) } } diff --git a/ui/src/app/pages/login/login.page.ts b/ui/src/app/pages/login/login.page.ts index a59e2aba4..14c2ceca3 100644 --- a/ui/src/app/pages/login/login.page.ts +++ b/ui/src/app/pages/login/login.page.ts @@ -2,7 +2,6 @@ import { Component } from '@angular/core' import { LoadingController } from '@ionic/angular' import { Subscription } from 'rxjs' import { AuthService } from 'src/app/services/auth.service' -import { PatchConnection, PatchDbService } from 'src/app/services/patch-db/patch-db.service' @Component({ selector: 'login', @@ -19,13 +18,8 @@ export class LoginPage { constructor ( private readonly authService: AuthService, private readonly loadingCtrl: LoadingController, - private readonly patch: PatchDbService, ) { } - ngOnInit () { - - } - ngOnDestroy () { if (this.loader) { this.loader.dismiss() @@ -52,16 +46,10 @@ export class LoginPage { try { await this.authService.login(this.password) - this.loader.message = 'Loading Embassy Data' this.password = '' - this.patchConnectionSub = this.patch.watchPatchConnection$().subscribe(status => { - if (status === PatchConnection.Disconnected) { - this.error = 'Connection failed' - this.loader.dismiss() - } - }) } catch (e) { this.error = e.message + } finally { this.loader.dismiss() } } diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index 7a0db298c..75dcc5d9c 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -6,6 +6,7 @@ import { MarketplaceListPage } from './marketplace-list.page' import { SharingModule } from '../../../modules/sharing.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 { FormsModule } from '@angular/forms' const routes: Routes = [ { @@ -18,6 +19,7 @@ const routes: Routes = [ imports: [ CommonModule, IonicModule, + FormsModule, RouterModule.forChild(routes), StatusComponentModule, SharingModule, diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index df3038dcb..27af02dc8 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -9,7 +9,14 @@

Embassy Marketplace

- + + + + + + + + diff --git a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 6b07f5631..dab21c2b8 100644 --- a/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -88,10 +88,11 @@ export class MarketplaceListPage { e.target.complete() } - async search (e?: any): Promise { - this.query = e.target.value || undefined - this.page = 1 + async search (): Promise { + if (!this.query) return this.pkgsLoading = true + this.category = undefined + this.page = 1 await this.getPkgs() } @@ -134,9 +135,9 @@ export class MarketplaceListPage { } async switchCategory (category: string): Promise { - this.pkgs = [] - this.category = category this.pkgsLoading = true + this.category = category + this.query = undefined this.page = 1 await this.getPkgs() } diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index 2d825c960..9bd69a8f5 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -581,56 +581,58 @@ export module Mock { export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = Object.values(Mock.MarketplacePkgs).map(service => service['latest']) - export const bitcoinproxy: PackageDataEntry = { - state: PackageState.Installed, - 'static-files': { - license: 'licenseUrl', // /public/package-data/bitcoinproxy/0.21.1/LICENSE.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': { }, + export const Pkgs: { [key: string]: PackageDataEntry } = { + 'bitcoin-proxy': { + state: PackageState.Installed, + 'static-files': { + license: 'licenseUrl', // /public/package-data/bitcoinproxy/0.21.1/LICENSE.md, + icon: 'assets/img/service-icons/bitcoin-proxy.png', + instructions: 'instructionsUrl', // /public/package-data/bitcoinproxy/0.2.2/INSTRUCTIONS.md }, manifest: MockManifestBitcoinProxy, - 'interface-addresses': { - rpc: { - 'tor-address': 'bitcoinproxy-rpc-address.onion', - 'lan-address': 'bitcoinproxy-rpc-address.local', - }, - }, - 'system-pointers': [], - 'current-dependents': { - 'lnd': { - pointers: [], - 'health-checks': [], - }, - }, - 'current-dependencies': { - 'bitcoind': { - 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', + installed: { + status: { + configured: true, + main: { + status: PackageMainStatus.Running, + started: new Date().toISOString(), + health: { }, + }, + 'dependency-errors': { }, + }, + manifest: MockManifestBitcoinProxy, + 'interface-addresses': { + rpc: { + 'tor-address': 'bitcoinproxy-rpc-address.onion', + 'lan-address': 'bitcoinproxy-rpc-address.local', + }, + }, + 'system-pointers': [], + 'current-dependents': { + 'lnd': { + pointers: [], + 'health-checks': [], + }, + }, + 'current-dependencies': { + 'bitcoind': { + 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 = [ @@ -1455,62 +1457,62 @@ export module Mock { // '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, - }, - }, - }, - manifest: MockManifestLnd, - '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': [], - }, - 'bitcoin-proxy': { - pointers: [], - 'health-checks': [], - }, - }, - 'dependency-info': { - 'bitcoind': { - manifest: Mock.MockManifestBitcoind, - icon: 'assets/img/service-icons/bitcoind.png', - }, - 'bitcoin-proxy': { - manifest: Mock.MockManifestBitcoinProxy, - icon: 'assets/img/service-icons/bitcoin-proxy.png', - }, - }, - }, - '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, + // }, + // }, + // }, + // manifest: MockManifestLnd, + // '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': [], + // }, + // 'bitcoin-proxy': { + // pointers: [], + // 'health-checks': [], + // }, + // }, + // 'dependency-info': { + // 'bitcoind': { + // manifest: Mock.MockManifestBitcoind, + // icon: 'assets/img/service-icons/bitcoind.png', + // }, + // 'bitcoin-proxy': { + // manifest: Mock.MockManifestBitcoinProxy, + // icon: 'assets/img/service-icons/bitcoin-proxy.png', + // }, + // }, + // }, + // 'install-progress': undefined, + // } // export const DbDump: RR.GetDumpRes = { // id: 1, diff --git a/ui/src/app/services/api/embassy-api.service.ts b/ui/src/app/services/api/embassy-api.service.ts index 97a029cc5..4e2060444 100644 --- a/ui/src/app/services/api/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy-api.service.ts @@ -13,6 +13,8 @@ export abstract class ApiService implements Source, Http { return this.sync.asObservable() } + connectionMade$ = new Subject() + // for getting static files: ex icons, instructions, licenses abstract getStatic (url: string): Promise @@ -195,6 +197,7 @@ export abstract class ApiService implements Source, Http { throw e }) .then(({ response, revision }) => { + this.connectionMade$.next() if (revision) this.sync.next(revision) return response }) diff --git a/ui/src/app/services/api/embassy-live-api.service.ts b/ui/src/app/services/api/embassy-live-api.service.ts index 005ed94cd..52fcca814 100644 --- a/ui/src/app/services/api/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy-live-api.service.ts @@ -96,6 +96,7 @@ export class LiveApiService extends ApiService { } async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise { + if (params.query) params.category = undefined return this.http.httpRequest({ method: Method.GET, url: '/marketplace/package/index', 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 042186985..b79f6feb2 100644 --- a/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy-mock-api.service.ts @@ -335,7 +335,7 @@ export class MockApiService extends ApiService { 'unpack-complete': false, } const pkg: PackageDataEntry = { - ...Mock[params.id], + ...Mock.Pkgs[params.id], state: PackageState.Installing, 'install-progress': initialProgress, } 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 5ddb61d80..6fb2efb3a 100644 --- a/ui/src/app/services/patch-db/patch-db.service.ts +++ b/ui/src/app/services/patch-db/patch-db.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, InjectionToken } from '@angular/core' import { Bootstrapper, PatchDB, Source, Store } from 'patch-db-client' -import { BehaviorSubject, Observable, of, Subscription } from 'rxjs' -import { catchError, debounceTime, finalize, map, tap } from 'rxjs/operators' +import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs' +import { catchError, debounceTime, delay, filter, finalize, map, take, tap, timeout } from 'rxjs/operators' import { pauseFor } from 'src/app/util/misc.util' import { ApiService } from '../api/embassy-api.service' import { DataModel } from './data-model' @@ -42,18 +42,30 @@ export class PatchDbService { start (): void { // make sure everything is stopped before initializing if (this.patchSub) { + console.log('Retrying') this.patchSub.unsubscribe() this.patchSub = undefined } - console.log('Retrying') try { - this.patchSub = this.patchDb.sync$() - .pipe(debounceTime(500)) - .subscribe({ - next: cache => { + const connectedSub$ = this.patchDb.connectionMade$() + .pipe( + tap(() => { this.patchConnection$.next(PatchConnection.Connected) + }), + timeout(30000), + take(1), + ) + + const updateSub$ = this.patchDb.sync$() + .pipe( + debounceTime(500), + tap(cache => { this.bootstrapper.update(cache) - }, + }), + ) + + this.patchSub = combineLatest([connectedSub$, updateSub$]) + .subscribe({ error: async e => { console.error('patch-db-sync sub ERROR', e) this.patchConnection$.next(PatchConnection.Disconnected) diff --git a/ui/src/app/util/misc.util.ts b/ui/src/app/util/misc.util.ts index 8ff4e12a6..e370fa361 100644 --- a/ui/src/app/util/misc.util.ts +++ b/ui/src/app/util/misc.util.ts @@ -171,4 +171,19 @@ export const exists = (t: any) => { export type DeepPartial = { [k in keyof T]?: DeepPartial +} + +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 + } } \ No newline at end of file