Files
start-os/web/projects/ui/src/app/services/dep-error.service.ts
Matt Hill 2ba56b8c59 Convert properties to an action (#2751)
* update actions response types and partially implement in UI

* further remove diagnostic ui

* convert action response nested to array

* prepare action res modal for Alex

* ad dproperties action for Bitcoin

* feat: add action success dialog (#2753)

* feat: add action success dialog

* mocks for string action res and hide properties from actions page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* return null

* remove properties from backend

* misc fixes

* make severity separate argument

* rename ActionRequest to ActionRequestOptions

* add clearRequests

* fix s9pk build

* remove config and properties, introduce action requests

* better ux, better moocks, include icons

* fix dependency types

* add variant for versionCompat

* fix dep icon display and patch operation display

* misc fixes

* misc fixes

* alpha 12

* honor provided input to set values in action

* fix: show full descriptions of action success items (#2758)

* fix type

* fix: fix build:deps command on Windows (#2752)

* fix: fix build:deps command on Windows

* fix: add escaped quotes

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>

* misc db compatibility fixes

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2024-10-17 13:31:56 -06:00

171 lines
4.2 KiB
TypeScript

import { Injectable } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
InstalledState,
PackageDataEntry,
} from './patch-db/data-model'
import * as deepEqual from 'fast-deep-equal'
import { isInstalled } from '../util/get-package-data'
import { DependencyError } from './api/api.types'
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
export type PkgDependencyErrors = Record<string, DependencyError | null>
@Injectable({
providedIn: 'root',
})
export class DepErrorService {
readonly depErrors$ = this.patch.watch$('packageData').pipe(
map(pkgs =>
Object.keys(pkgs)
.map(id => ({
id,
depth: dependencyDepth(pkgs, id),
}))
.sort((a, b) => (b.depth > a.depth ? -1 : 1))
.reduce(
(errors, { id }): AllDependencyErrors => ({
...errors,
[id]: this.getDepErrors(pkgs, id, errors),
}),
{} as AllDependencyErrors,
),
),
distinctUntilChanged(deepEqual),
shareReplay({ bufferSize: 1, refCount: true }),
)
constructor(
private readonly exver: Exver,
private readonly patch: PatchDB<DataModel>,
) {}
getPkgDepErrors$(pkgId: string) {
return this.depErrors$.pipe(
map(depErrors => depErrors[pkgId]),
distinctUntilChanged(deepEqual),
)
}
private getDepErrors(
pkgs: DataModel['packageData'],
pkgId: string,
outerErrors: AllDependencyErrors,
): PkgDependencyErrors {
const pkg = pkgs[pkgId]
if (!isInstalled(pkg)) return {}
return currentDeps(pkgs, pkgId).reduce(
(innerErrors, depId): PkgDependencyErrors => ({
...innerErrors,
[depId]: this.getDepError(pkgs, pkg, depId, outerErrors),
}),
{} as PkgDependencyErrors,
)
}
private getDepError(
pkgs: DataModel['packageData'],
pkg: PackageDataEntry<InstalledState>,
depId: string,
outerErrors: AllDependencyErrors,
): DependencyError | null {
const dep = pkgs[depId]
// not installed
if (!dep || dep.stateInfo.state !== 'installed') {
return {
type: 'notInstalled',
}
}
const currentDep = pkg.currentDependencies[depId]
const depManifest = dep.stateInfo.manifest
// incorrect version
if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) {
if (
depManifest.satisfies.some(
v => !this.exver.satisfies(v, currentDep.versionRange),
)
) {
return {
type: 'incorrectVersion',
expected: currentDep.versionRange,
received: depManifest.version,
}
}
}
// action required
if (
Object.values(pkg.requestedActions).some(
a =>
a.active &&
a.request.packageId === depId &&
a.request.severity === 'critical',
)
) {
return {
type: 'actionRequired',
}
}
const depStatus = dep.status.main
// not running
if (depStatus !== 'running' && depStatus !== 'starting') {
return {
type: 'notRunning',
}
}
// health check failure
if (depStatus === 'running' && currentDep.kind === 'running') {
for (let id of currentDep.healthChecks) {
const check = dep.status.health[id]
if (check?.result !== 'success') {
return {
type: 'healthChecksFailed',
check,
}
}
}
}
// transitive
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
Object.values(outerErrors[transitiveId]).some(err => !!err),
)
if (transitiveError) {
return {
type: 'transitive',
}
}
return null
}
}
function currentDeps(pkgs: DataModel['packageData'], id: string): string[] {
return Object.keys(pkgs[id]?.currentDependencies || {}).filter(
depId => depId !== id,
)
}
function dependencyDepth(
pkgs: DataModel['packageData'],
id: string,
depth = 0,
): number {
return currentDeps(pkgs, id).reduce(
(prev, depId) => dependencyDepth(pkgs, depId, prev + 1),
depth,
)
}