mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
* refector addresses to not need gateways array * fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color * fix: fix action results textfields --------- Co-authored-by: waterplea <alexander@inkin.ru>
209 lines
5.0 KiB
TypeScript
209 lines
5.0 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 deepEqual from 'fast-deep-equal'
|
|
import { Observable } from 'rxjs'
|
|
import { isInstalled } from 'src/app/utils/get-package-data'
|
|
import { T } from '@start9labs/start-sdk'
|
|
|
|
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
|
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
|
|
|
export type DependencyError =
|
|
| DependencyErrorNotInstalled
|
|
| DependencyErrorNotRunning
|
|
| DependencyErrorIncorrectVersion
|
|
| DependencyErrorTaskRequired
|
|
| DependencyErrorHealthChecksFailed
|
|
| DependencyErrorTransitive
|
|
|
|
export type DependencyErrorNotInstalled = {
|
|
type: 'notInstalled'
|
|
}
|
|
|
|
export type DependencyErrorNotRunning = {
|
|
type: 'notRunning'
|
|
}
|
|
|
|
export type DependencyErrorIncorrectVersion = {
|
|
type: 'incorrectVersion'
|
|
expected: string // version range
|
|
received: string // version
|
|
}
|
|
|
|
export interface DependencyErrorTaskRequired {
|
|
type: 'taskRequired'
|
|
}
|
|
|
|
export type DependencyErrorHealthChecksFailed = {
|
|
type: 'healthChecksFailed'
|
|
check: T.NamedHealthCheckResult
|
|
}
|
|
|
|
export type DependencyErrorTransitive = {
|
|
type: 'transitive'
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class DepErrorService {
|
|
readonly depErrors$: Observable<AllDependencyErrors> = 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(1),
|
|
)
|
|
|
|
constructor(
|
|
private readonly exver: Exver,
|
|
private readonly patch: PatchDB<DataModel>,
|
|
) {}
|
|
|
|
getPkgDepErrors$(pkgId: string): Observable<PkgDependencyErrors> {
|
|
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 (!pkg || !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
|
|
const expected = currentDep?.versionRange || ''
|
|
|
|
// incorrect version
|
|
if (
|
|
!this.exver.satisfies(depManifest.version, expected) &&
|
|
!depManifest.satisfies.some(v => this.exver.satisfies(v, expected))
|
|
) {
|
|
return {
|
|
expected,
|
|
type: 'incorrectVersion',
|
|
received: depManifest.version,
|
|
}
|
|
}
|
|
|
|
// action required
|
|
if (
|
|
Object.values(pkg.tasks)
|
|
.filter(t => !!t)
|
|
.some(
|
|
t =>
|
|
t.active &&
|
|
t.task.packageId === depId &&
|
|
t.task.severity === 'critical',
|
|
)
|
|
) {
|
|
return {
|
|
type: 'taskRequired',
|
|
}
|
|
}
|
|
|
|
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 && 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,
|
|
)
|
|
}
|