diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9553271bc..fceedfaec 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -37,6 +37,7 @@
"cbor-web": "^8.1.0",
"core-js": "^3.21.1",
"dompurify": "^2.3.6",
+ "fast-deep-equal": "^3.1.3",
"fast-json-patch": "^3.1.1",
"fuse.js": "^6.4.6",
"jose": "^4.9.0",
@@ -7512,6 +7513,20 @@
"clone": "^1.0.2"
}
},
+ "node_modules/define-data-property": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
+ "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"devOptional": true,
@@ -7521,10 +7536,12 @@
}
},
"node_modules/define-properties": {
- "version": "1.1.4",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
- "license": "MIT",
"dependencies": {
+ "define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
@@ -8208,7 +8225,8 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.2.12",
@@ -8560,12 +8578,14 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.1.3",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
+ "has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
},
"funding": {
@@ -8704,6 +8724,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.10",
"devOptional": true,
@@ -8758,6 +8790,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.0.3",
"dev": true,
diff --git a/frontend/package.json b/frontend/package.json
index 3b03dd26f..2e2beb413 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -62,6 +62,7 @@
"cbor-web": "^8.1.0",
"core-js": "^3.21.1",
"dompurify": "^2.3.6",
+ "fast-deep-equal": "^3.1.3",
"fast-json-patch": "^3.1.1",
"fuse.js": "^6.4.6",
"jose": "^4.9.0",
diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html
index 05e4a76cf..3bc38f762 100644
--- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html
+++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/app-list.page.html
@@ -27,7 +27,7 @@
sizeMd="6"
>
diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts
index 103845620..df48680af 100644
--- a/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts
+++ b/frontend/projects/ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts
@@ -1,10 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core'
-import { Observable, combineLatest } from 'rxjs'
-import { filter, map, startWith } from 'rxjs/operators'
-import {
- DataModel,
- PackageDataEntry,
-} from 'src/app/services/patch-db/data-model'
+import { Observable, combineLatest, firstValueFrom } from 'rxjs'
+import { map } from 'rxjs/operators'
+import { DataModel } from 'src/app/services/patch-db/data-model'
import { getPackageInfo, PkgInfo } from '../../../util/get-package-info'
import { PatchDB } from 'patch-db-client'
import { DepErrorService } from 'src/app/services/dep-error.service'
@@ -18,12 +15,10 @@ export class PackageInfoPipe implements PipeTransform {
private readonly depErrorService: DepErrorService,
) {}
- transform(pkg: PackageDataEntry): Observable {
+ transform(pkgId: string): Observable {
return combineLatest([
- this.patch
- .watch$('package-data', pkg.manifest.id)
- .pipe(filter(Boolean), startWith(pkg)),
- this.depErrorService.depErrors$,
+ this.patch.watch$('package-data', pkgId),
+ this.depErrorService.getPkgDepErrors$(pkgId),
]).pipe(map(([pkg, depErrors]) => getPackageInfo(pkg, depErrors)))
}
}
diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts
index 4257f916d..19b29c1f6 100644
--- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts
+++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts
@@ -21,7 +21,7 @@ import { DependentInfo } from 'src/app/types/dependent-info'
import {
DepErrorService,
DependencyErrorType,
- PackageDependencyErrors,
+ PkgDependencyErrors,
} from 'src/app/services/dep-error.service'
import { combineLatest } from 'rxjs'
@@ -50,15 +50,14 @@ export class AppShowPage {
private readonly pkgId = getPkgId(this.route)
readonly pkgPlus$ = combineLatest([
- this.patch.watch$('package-data'),
- this.depErrorService.depErrors$,
+ this.patch.watch$('package-data', this.pkgId),
+ this.depErrorService.getPkgDepErrors$(this.pkgId),
]).pipe(
- tap(([pkgs, _]) => {
+ tap(([pkg, _]) => {
// if package disappears, navigate to list page
- if (!pkgs[this.pkgId]) this.navCtrl.navigateRoot('/services')
+ if (!pkg) this.navCtrl.navigateRoot('/services')
}),
- map(([pkgs, depErrors]) => {
- const pkg = pkgs[this.pkgId]
+ map(([pkg, depErrors]) => {
return {
pkg,
dependencies: this.getDepInfo(pkg, depErrors),
@@ -93,7 +92,7 @@ export class AppShowPage {
private getDepInfo(
pkg: PackageDataEntry,
- depErrors: PackageDependencyErrors,
+ depErrors: PkgDependencyErrors,
): DependencyInfo[] {
const pkgInstalled = pkg.installed
@@ -107,7 +106,7 @@ export class AppShowPage {
private getDepValues(
pkgInstalled: InstalledPackageDataEntry,
depId: string,
- depErrors: PackageDependencyErrors,
+ depErrors: PkgDependencyErrors,
): DependencyInfo {
const { errorText, fixText, fixAction } = this.getDepErrors(
pkgInstalled,
@@ -134,10 +133,10 @@ export class AppShowPage {
private getDepErrors(
pkgInstalled: InstalledPackageDataEntry,
depId: string,
- depErrors: PackageDependencyErrors,
+ depErrors: PkgDependencyErrors,
) {
const pkgManifest = pkgInstalled.manifest
- const depError = depErrors[pkgInstalled.manifest.id][depId]
+ const depError = depErrors[depId]
let errorText: string | null = null
let fixText: string | null = null
diff --git a/frontend/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts b/frontend/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts
index f69a0407b..e4f456c01 100644
--- a/frontend/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts
+++ b/frontend/projects/ui/src/app/pages/widgets/built-in/health/health.component.ts
@@ -7,6 +7,8 @@ import {
} from 'src/app/services/patch-db/data-model'
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info'
+import { combineLatest } from 'rxjs'
+import { DepErrorService } from 'src/app/services/dep-error.service'
@Component({
selector: 'widget-health',
@@ -23,31 +25,32 @@ export class HealthComponent {
'Transitioning',
] as const
- readonly data$ = inject(PatchDB)
- .watch$('package-data')
- .pipe(
- map(data => {
- const pkgs = Object.values(data).map(
- pkg => getPackageInfo(pkg, {}), // @TODO hack because not currently using widget
- )
- const result = this.labels.reduce>(
- (acc, label) => ({
- ...acc,
- [label]: this.getCount(label, pkgs),
- }),
- {},
- )
+ readonly data$ = combineLatest([
+ inject(PatchDB).watch$('package-data'),
+ inject(DepErrorService).depErrors$,
+ ]).pipe(
+ map(([data, depErrors]) => {
+ const pkgs = Object.values(data).map(pkg =>
+ getPackageInfo(pkg, depErrors[pkg.manifest.id]),
+ )
+ const result = this.labels.reduce>(
+ (acc, label) => ({
+ ...acc,
+ [label]: this.getCount(label, pkgs),
+ }),
+ {},
+ )
- result['Healthy'] =
- pkgs.length -
- result['Error'] -
- result['Needs Attention'] -
- result['Stopped'] -
- result['Transitioning']
+ result['Healthy'] =
+ pkgs.length -
+ result['Error'] -
+ result['Needs Attention'] -
+ result['Stopped'] -
+ result['Transitioning']
- return this.labels.map(label => result[label])
- }),
- )
+ return this.labels.map(label => result[label])
+ }),
+ )
private getCount(label: string, pkgs: PkgInfo[]): number {
switch (label) {
diff --git a/frontend/projects/ui/src/app/services/dep-error.service.ts b/frontend/projects/ui/src/app/services/dep-error.service.ts
index 733b0a377..1b1880f72 100644
--- a/frontend/projects/ui/src/app/services/dep-error.service.ts
+++ b/frontend/projects/ui/src/app/services/dep-error.service.ts
@@ -1,6 +1,14 @@
import { Injectable } from '@angular/core'
import { Emver } from '@start9labs/shared'
-import { map, shareReplay } from 'rxjs/operators'
+import {
+ distinctUntilChanged,
+ filter,
+ map,
+ pairwise,
+ shareReplay,
+ startWith,
+ tap,
+} from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
@@ -9,9 +17,10 @@ import {
InstalledPackageDataEntry,
PackageMainStatus,
} from './patch-db/data-model'
+import * as deepEqual from 'fast-deep-equal'
-export type PackageDependencyErrors = Record
-export type DependencyErrors = Record
+export type AllDependencyErrors = Record
+export type PkgDependencyErrors = Record
@Injectable({
providedIn: 'root',
@@ -26,13 +35,14 @@ export class DepErrorService {
}))
.sort((a, b) => (b.depth > a.depth ? -1 : 1))
.reduce(
- (errors, { id }): PackageDependencyErrors => ({
+ (errors, { id }): AllDependencyErrors => ({
...errors,
[id]: this.getDepErrors(pkgs, id, errors),
}),
- {} as PackageDependencyErrors,
+ {} as AllDependencyErrors,
),
),
+ distinctUntilChanged(deepEqual),
shareReplay(1),
)
@@ -41,21 +51,28 @@ export class DepErrorService {
private readonly patch: PatchDB,
) {}
+ getPkgDepErrors$(pkgId: string) {
+ return this.depErrors$.pipe(
+ map(depErrors => depErrors[pkgId]),
+ distinctUntilChanged(deepEqual),
+ )
+ }
+
private getDepErrors(
pkgs: DataModel['package-data'],
pkgId: string,
- outerErrors: PackageDependencyErrors,
- ): DependencyErrors {
+ outerErrors: AllDependencyErrors,
+ ): PkgDependencyErrors {
const pkgInstalled = pkgs[pkgId].installed
if (!pkgInstalled) return {}
return currentDeps(pkgs, pkgId).reduce(
- (innerErrors, depId): DependencyErrors => ({
+ (innerErrors, depId): PkgDependencyErrors => ({
...innerErrors,
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
}),
- {} as DependencyErrors,
+ {} as PkgDependencyErrors,
)
}
@@ -63,7 +80,7 @@ export class DepErrorService {
pkgs: DataModel['package-data'],
pkgInstalled: InstalledPackageDataEntry,
depId: string,
- outerErrors: PackageDependencyErrors,
+ outerErrors: AllDependencyErrors,
): DependencyError | null {
const depInstalled = pkgs[depId]?.installed
diff --git a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts
index ce415bcbd..28c58a809 100644
--- a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts
+++ b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts
@@ -1,13 +1,12 @@
import { isEmptyObject } from '@start9labs/shared'
import {
- InstalledPackageDataEntry,
MainStatusStarting,
PackageDataEntry,
PackageMainStatus,
PackageState,
Status,
} from 'src/app/services/patch-db/data-model'
-import { PackageDependencyErrors } from './dep-error.service'
+import { PkgDependencyErrors } from './dep-error.service'
export interface PackageStatus {
primary: PrimaryStatus
@@ -17,7 +16,7 @@ export interface PackageStatus {
export function renderPkgStatus(
pkg: PackageDataEntry,
- depErrors: PackageDependencyErrors,
+ depErrors: PkgDependencyErrors,
): PackageStatus {
let primary: PrimaryStatus
let dependency: DependencyStatus | null = null
@@ -25,7 +24,7 @@ export function renderPkgStatus(
if (pkg.state === PackageState.Installed && pkg.installed) {
primary = getPrimaryStatus(pkg.installed.status)
- dependency = getDependencyStatus(pkg.installed, depErrors)
+ dependency = getDependencyStatus(depErrors)
health = getHealthStatus(
pkg.installed.status,
!isEmptyObject(pkg.manifest['health-checks']),
@@ -47,11 +46,8 @@ function getPrimaryStatus(status: Status): PrimaryStatus {
}
}
-function getDependencyStatus(
- pkg: InstalledPackageDataEntry,
- depErrors: PackageDependencyErrors,
-): DependencyStatus {
- return Object.values(depErrors[pkg.manifest.id]).some(err => !!err)
+function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
+ return Object.values(depErrors).some(err => !!err)
? DependencyStatus.Warning
: DependencyStatus.Satisfied
}
diff --git a/frontend/projects/ui/src/app/util/get-package-info.ts b/frontend/projects/ui/src/app/util/get-package-info.ts
index f98d969fb..14901f201 100644
--- a/frontend/projects/ui/src/app/util/get-package-info.ts
+++ b/frontend/projects/ui/src/app/util/get-package-info.ts
@@ -10,11 +10,11 @@ import {
import { ProgressData } from 'src/app/types/progress-data'
import { Subscription } from 'rxjs'
import { packageLoadingProgress } from './package-loading-progress'
-import { PackageDependencyErrors } from '../services/dep-error.service'
+import { PkgDependencyErrors } from '../services/dep-error.service'
export function getPackageInfo(
entry: PackageDataEntry,
- depErrors: PackageDependencyErrors,
+ depErrors: PkgDependencyErrors,
): PkgInfo {
const statuses = renderPkgStatus(entry, depErrors)
const primaryRendering = PrimaryRendering[statuses.primary]