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
}