diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html index 40d1b903a..860f1f4fe 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html @@ -109,9 +109,9 @@

{{ vars.lanAddress }}

-

- Testing Connection - +

+ Testing Connection +

diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss index 31b6fdbc6..9d1390694 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss @@ -66,3 +66,9 @@ --handle-width: 0.9em; --handle-height: 0.9em; } + +.item-interactive-disabled:not(.item-multiple-inputs) ion-label { + cursor: default; + opacity: 1 !important; + pointer-events: auto !important; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts index bbf155dbe..f60ebf21c 100644 --- a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts @@ -13,13 +13,13 @@ import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/a import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject } from 'rxjs' import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' -import { catchError, concatMap, delay, distinctUntilChanged, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators' +import { catchError, concatMap, delay, distinctUntilChanged, filter, map, mergeMap, retryWhen, switchMap, take, tap } from 'rxjs/operators' import { Cleanup } from 'src/app/util/cleanup' import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component' import { Emver } from 'src/app/services/emver.service' import { displayEmver } from 'src/app/pipes/emver.pipe' import { ConfigService } from 'src/app/services/config.service' -import { squash } from 'src/app/util/rxjs.util' +import { concatObservableValues, squash } from 'src/app/util/rxjs.util' @Component({ selector: 'app-installed-show', templateUrl: './app-installed-show.page.html', @@ -79,36 +79,28 @@ export class AppInstalledShowPage extends Cleanup { concatMap(app => merge( this.syncWhenDependencyInstalls(), - // new lan info from sync daemon - combineLatest([app.lanEnabled, this.$lanConnected$, app.status, this.$testingLanConnection$]).pipe( - filter(([_, __, s, alreadyConnecting]) => s === AppStatus.RUNNING && !alreadyConnecting), - concatMap(([enabled, connected]) => { - // console.log('enabled', enabled) - // console.log('connected', connected) + // new lan info or status info from sync daemon + combineLatest([app.lanEnabled, app.status]).pipe( + concatObservableValues([this.$lanConnected$, this.$testingLanConnection$]), + concatMap(([enabled, status, connected, alreadyConnecting]) => { + if (status !== AppStatus.RUNNING) return of(this.$lanConnected$.next(false)) + if (alreadyConnecting) return of() if (enabled && !connected) return markAsLoadingDuring$(this.$testingLanConnection$, this.testLanConnection()) if (!enabled && connected) return of(this.$lanConnected$.next(false)) return of() }), ), // toggle lan - combineLatest([this.$lanToggled$, app.lanEnabled, this.$testingLanConnection$]).pipe( - filter(([_, __, alreadyLoading]) => !alreadyLoading), - map(([e, _]) => [(e as any).detail.checked, _]), - distinctUntilChanged(([toggled1], [toggled2]) => toggled1 === toggled2), - // if the app is already in the desired state, we bail - // this can happen because ionChange triggers when the [checked] value changes - filter(([uiEnabled, appEnabled]) => (uiEnabled && !appEnabled) || (!uiEnabled && appEnabled)), - concatMap( ([enabled]) => { - let o: Observable - if (enabled) { - o = this.enableLan().pipe(concatMap(() => this.testLanConnection())) - } else { - o = this.disableLan() - } - return markAsLoadingDuring$(this.$testingLanConnection$, o).pipe( - catchError(e => this.setError(e)), - ) + this.$lanToggled$.pipe( + map(toggleEvent => (toggleEvent as any).detail.checked), + concatObservableValues([app.lanEnabled, this.$testingLanConnection$]), + traceWheel('toggle'), + map( ([uiEnabled, appEnabled, alreadyConnecting]) => { + if (!alreadyConnecting && uiEnabled && !appEnabled) return this.enableLan().pipe(concatMap(() => this.testLanConnection())) + if (!alreadyConnecting && !uiEnabled) return this.disableLan() //do this even if app already disabled because of appModel update timeout hack. + return of() }), + concatMap((o: Observable) => this.testLanLoader(o)), ), ), ), //must be final in stack @@ -117,6 +109,10 @@ export class AppInstalledShowPage extends Cleanup { ) } + testLanLoader (o: Observable): Observable { + return markAsLoadingDuring$(this.$testingLanConnection$, o).pipe(catchError(e => this.setError(e))) + } + testLanConnection () : Observable { if (!this.app.lanAddress) return of() diff --git a/ui/src/app/services/api/mock-api.service.ts b/ui/src/app/services/api/mock-api.service.ts index 15deb8c9d..63c8c613d 100644 --- a/ui/src/app/services/api/mock-api.service.ts +++ b/ui/src/app/services/api/mock-api.service.ts @@ -45,8 +45,8 @@ export class MockApiService extends ApiService { async testConnection (): Promise { console.log('testing connection') this.testCounter ++ - await pauseFor(10000000) - if (this.testCounter > 3) { + await pauseFor(500) + if (this.testCounter > 2) { return true } else { throw new Error('Not Connected') diff --git a/ui/src/app/services/sync.service.ts b/ui/src/app/services/sync.service.ts index 46f2377ba..af2bd051d 100644 --- a/ui/src/app/services/sync.service.ts +++ b/ui/src/app/services/sync.service.ts @@ -15,9 +15,6 @@ export class SyncDaemon { private readonly syncInterval = 5000 private readonly $sync$ = new BehaviorSubject(false) - // emits on every successful sync - private readonly $synced$ = new Subject() - constructor ( private readonly apiService: ApiService, private readonly serverModel: ServerModel, @@ -39,15 +36,10 @@ export class SyncDaemon { return from(this.getServerAndApps()).pipe( concatMap(() => this.syncNotifier.handleSpecial(this.serverModel.peek())), concatMap(() => this.startupAlertsNotifier.runChecks(this.serverModel.peek())), - tap(() => this.$synced$.next()), catchError(e => of(console.error(`Exception in sync service`, e))), ) } - watchSynced (): Observable { - return this.$synced$.asObservable() - } - private async getServerAndApps (): Promise { const now = new Date() const [serverRes, appsRes] = await tryAll([ diff --git a/ui/src/app/util/rxjs.util.ts b/ui/src/app/util/rxjs.util.ts index 5179fa5be..020144d5a 100644 --- a/ui/src/app/util/rxjs.util.ts +++ b/ui/src/app/util/rxjs.util.ts @@ -1,5 +1,5 @@ -import { Observable, from, interval, race, OperatorFunction, Observer, BehaviorSubject } from 'rxjs' -import { take, map, switchMap, delay, tap, concatMap } from 'rxjs/operators' +import { Observable, from, interval, race, OperatorFunction, Observer, combineLatest } from 'rxjs' +import { take, map, concatMap } from 'rxjs/operators' export function fromAsync$ (async: (s: S) => Promise, s: S): Observable export function fromAsync$ (async: () => Promise): Observable @@ -17,14 +17,48 @@ export function emitAfter$ (ms: number): Observable { return interval(ms).pipe(take(1)) } +// throws unless source observable emits withing timeout export function throwIn (timeout: number): OperatorFunction { return o => race( o, emitAfter$(timeout).pipe(map(() => { throw new Error('timeout') } ))) } +// o.pipe(squash) : Observable regardless of o emission type. export const squash = map(() => { }) +/* + The main purpose of fromSync$ is to normalize error handling during a sequence + of actions beginning with a standard synchronous action and followed by a pipe. + For example, imagine we have `f(s: S): T` which might throw, and we wish to define the following: + ``` + function observableF(t: T): Observable { + const s = f(t) + return someFunctionReturningAnObservable(s) + } + ``` + + For the caller, `observableF(t).pipe(...).subscribe({ error: e => console.error('observable errored!') })` + might throw an error from `f` which does not result in 'observable errored!' being logged. + We could fix this with... + ``` + function observableF(t: T): Observable { + try { + const s = f(t) + return someFunctionReturningAnObservable(s) + } catch(e) { + return throwError(e) + } + } + ``` + + or we could use fromSync as below + ``` + function observableF(t: T): Observable { + return fromSync$(f, t).concatMap(someFunctionReturningAnObservable) + } + ``` +*/ export function fromSync$ (sync: (s: S) => T, s: S): Observable export function fromSync$ (sync: () => T): Observable export function fromSync$ (sync: (s: S) => T, s?: S): Observable { @@ -38,28 +72,23 @@ export function fromSync$ (sync: (s: S) => T, s?: S): Observable { }) } -export function onCooldown (cooldown: number, o: () => Observable): Observable { - - const $trigger$ = new BehaviorSubject(true) - $trigger$.subscribe(t => console.log('triggering', t)) - return $trigger$.pipe( - switchMap(_ => - o().pipe( - delay(cooldown), - tap(() => $trigger$.next(true)), - ), +/* + this function concats the current values (e.g in behavior subjects) or next values (in traditional observables) to a collection of values in a pipe. + e.g. if t: Observable, and o1: Observable o2: Observable then t.pipe(concatObservableValues([o1, o2])): Observable<[T1, O1, O2]> and emits iff t emits. + Note that the standard combineLatest([t, o1, o2]) is also of type Observable<[T, O2, O2]>, but this observable triggers when any of t, o1, o2 emits. +*/ +export function concatObservableValues (observables: [Observable]): OperatorFunction +export function concatObservableValues (observables: [Observable]): OperatorFunction<[T], [T, O]> +export function concatObservableValues (observables: [Observable]): OperatorFunction<[T1, T2], [T1, T2, O]> +export function concatObservableValues (observables: [Observable, Observable]): OperatorFunction<[T], [T, O1, O2]> +export function concatObservableValues (observables: [Observable, Observable]): OperatorFunction<[T1, T2], [T1, T2, O1, O2]> +export function concatObservableValues (observables: Observable[]): OperatorFunction { + return o => o.pipe(concatMap(args => combineLatest(observables).pipe( + map(obs => { + if (!(args instanceof Array)) return [args, ...obs] + return [...args, ...obs] + }), + take(1), ), - ) -} - - -export function bindPipe (o: Observable, then: (t: T) => Observable): Observable -export function bindPipe (o: Observable, then1: (t: T) => Observable, then2: (s: S1) => Observable): Observable -export function bindPipe (o: Observable, then1: (t: T) => Observable, then2: (s: S1) => Observable, then3: (s: S2) => Observable): Observable -export function bindPipe (o: Observable, then1: (t: T) => Observable, then2: (s: S1) => Observable, then3: (s: S2) => Observable, then4: (s: S3) => Observable): Observable -export function bindPipe (o: Observable, ...thens: ((t: any) => Observable)[]): Observable { - const concatted = thens.map(m => concatMap(m)) - return concatted.reduce( (acc, next) => { - return acc.pipe(next) - }, o) + )) } \ No newline at end of file diff --git a/ui/use-mocks.json b/ui/use-mocks.json index dc204ef37..81031f548 100644 --- a/ui/use-mocks.json +++ b/ui/use-mocks.json @@ -1,5 +1,5 @@ { - "useMocks": true, + "useMocks": false, "mockOver": "lan", - "skipStartupAlerts": true + "skipStartupAlerts": false }