mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Refactor/patch db (#2415)
* the only way to begin is by beginning * chore: Convert over 3444 migration * fix imports * wip * feat: convert volume * convert: system.rs * wip(convert): Setup * wip properties * wip notifications * wip * wip migration * wip init * wip auth/control * wip action * wip control * wiip 034 * wip 344 * wip some more versions converted * feat: Reserialize the version of the db * wip rest of the versions * wip s9pk/manifest * wip wifi * chore: net/keys * chore: net/dns * wip net/dhcp * wip manager manager-map * gut dependency errors * wip update/mod * detect breakages locally for updates * wip: manager/mod * wip: manager/health * wip: backup/target/mod * fix: Typo addresses * clean control.rs * fix system package id * switch to btreemap for now * config wip * wip manager/mod * install wip Co-authored-by: J H <Blu-J@users.noreply.github.com> * chore: Update the last of the errors * feat: Change the prelude de to borrow * feat: Adding in some more things * chore: add to the prelude * chore: Small fixes * chore: Fixing the small errors * wip: Cleaning up check errors * wip: Fix some of the issues * chore: Fix setup * chore:fix version * chore: prelude, mod, http_reader * wip backup_bulk * chore: Last of the errors * upadte package.json * chore: changes needed for a build * chore: Removing some of the linting errors in the manager * chore: Some linting 101 * fix: Wrong order of who owns what * chore: Remove the unstable * chore: Remove the test in the todo * @dr-bonez did a refactoring on the backup * chore: Make sure that there can only be one override guard at a time * resolve most todos * wip: Add some more tracing to debug an error * wip: Use a mv instead of rename * wip: Revert some of the missing code segments found earlier * chore: Make the build * chore: Something about the lib looks like it iis broken * wip: More instrument and dev working * kill netdummy before creating it * better db analysis tools * fixes from testing * fix: Make add start the service * fix status after install * make wormhole * fix missing icon file * fix data url for icons * fix: Bad deser * bugfixes * fix: Backup * fix: Some of the restor * fix: Restoring works * update frontend patch-db types * hack it in (#2424) * hack it in * optimize * slightly cleaner * handle config pointers * dependency config errs * fix compat * cache docker * fix dependency expectation * fix dependency auto-config --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: J H <Blu-J@users.noreply.github.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
17478
frontend/package-lock.json
generated
17478
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,7 @@
|
||||
"node-jose": "^2.2.0",
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"rxjs": "^7.8.1",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
@@ -103,7 +103,7 @@
|
||||
"raw-loader": "^4.0.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.6.3",
|
||||
"typescript": "4.8.4",
|
||||
"webpack-bundle-analyzer": "^4.8.0"
|
||||
},
|
||||
"husky": {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="welcome-header">
|
||||
<h1>Welcome to StartOS</h1>
|
||||
</div>
|
||||
<widget-list></widget-list>
|
||||
<!-- <widget-list></widget-list> -->
|
||||
</ng-container>
|
||||
|
||||
<ng-template #list>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Observable, combineLatest } from 'rxjs'
|
||||
import { filter, map, startWith } from 'rxjs/operators'
|
||||
import {
|
||||
DataModel,
|
||||
@@ -7,16 +7,23 @@ import {
|
||||
} 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'
|
||||
|
||||
@Pipe({
|
||||
name: 'packageInfo',
|
||||
})
|
||||
export class PackageInfoPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
|
||||
return this.patch
|
||||
.watch$('package-data', pkg.manifest.id)
|
||||
.pipe(filter(Boolean), startWith(pkg), map(getPackageInfo))
|
||||
return combineLatest([
|
||||
this.patch
|
||||
.watch$('package-data', pkg.manifest.id)
|
||||
.pipe(filter(Boolean), startWith(pkg)),
|
||||
this.depErrorService.depErrors$,
|
||||
]).pipe(map(([pkg, depErrors]) => getPackageInfo(pkg, depErrors)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ import { AppShowAdditionalComponent } from './components/app-show-additional/app
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -36,8 +34,6 @@ const routes: Routes = [
|
||||
ProgressDataPipe,
|
||||
ToHealthChecksPipe,
|
||||
ToButtonsPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
AppShowHeaderComponent,
|
||||
AppShowProgressComponent,
|
||||
AppShowStatusComponent,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<ng-container *ngIf="pkg$ | async as pkg">
|
||||
<ng-container *ngIf="pkgPlus$ | async as pkgPlus">
|
||||
<!-- header -->
|
||||
<app-show-header [pkg]="pkg"></app-show-header>
|
||||
<app-show-header [pkg]="pkgPlus.pkg"></app-show-header>
|
||||
|
||||
<!-- content -->
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ion-content *ngIf="pkgPlus.pkg as pkg" class="ion-padding with-widgets">
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ng-container *ngIf="showProgress(pkg); else installed">
|
||||
<app-show-progress
|
||||
@@ -15,33 +15,27 @@
|
||||
|
||||
<!-- Installed -->
|
||||
<ng-template #installed>
|
||||
<ng-container *ngIf="pkg | toDependencies as dependencies">
|
||||
<ion-item-group *ngIf="pkg | toStatus as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status
|
||||
<ion-item-group *ngIf="pkgPlus.status as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkg]="pkg"
|
||||
[dependencies]="dependencies"
|
||||
[status]="status"
|
||||
></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkg]="pkg"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
*ngIf="dependencies.length"
|
||||
[dependencies]="dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
*ngIf="pkgPlus.dependencies.length"
|
||||
[dependencies]="pkgPlus.dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
|
||||
@@ -3,16 +3,37 @@ import { NavController } from '@ionic/angular'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
InstalledPackageDataEntry,
|
||||
Manifest,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { tap } from 'rxjs/operators'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { map, tap } from 'rxjs/operators'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import {
|
||||
DepErrorService,
|
||||
DependencyErrorType,
|
||||
PackageDependencyErrors,
|
||||
} from 'src/app/services/dep-error.service'
|
||||
import { combineLatest } from 'rxjs'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
@@ -28,10 +49,21 @@ const STATES = [
|
||||
export class AppShowPage {
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
tap(pkg => {
|
||||
readonly pkgPlus$ = combineLatest([
|
||||
this.patch.watch$('package-data'),
|
||||
this.depErrorService.depErrors$,
|
||||
]).pipe(
|
||||
tap(([pkgs, _]) => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) this.navCtrl.navigateRoot('/services')
|
||||
if (!pkgs[this.pkgId]) this.navCtrl.navigateRoot('/services')
|
||||
}),
|
||||
map(([pkgs, depErrors]) => {
|
||||
const pkg = pkgs[this.pkgId]
|
||||
return {
|
||||
pkg,
|
||||
dependencies: this.getDepInfo(pkg, depErrors),
|
||||
status: renderPkgStatus(pkg, depErrors),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -39,6 +71,8 @@ export class AppShowPage {
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly modalService: ModalService,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
) {}
|
||||
|
||||
isInstalled({ state }: PackageDataEntry): boolean {
|
||||
@@ -56,4 +90,136 @@ export class AppShowPage {
|
||||
showProgress({ state }: PackageDataEntry): boolean {
|
||||
return STATES.includes(state)
|
||||
}
|
||||
|
||||
private getDepInfo(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): DependencyInfo[] {
|
||||
const pkgInstalled = pkg.installed
|
||||
|
||||
if (!pkgInstalled) return []
|
||||
|
||||
return Object.keys(pkgInstalled['current-dependencies'])
|
||||
.filter(id => !!pkgInstalled.manifest.dependencies[id])
|
||||
.map(id => this.getDepValues(pkgInstalled, id, depErrors))
|
||||
}
|
||||
|
||||
private getDepValues(
|
||||
pkgInstalled: InstalledPackageDataEntry,
|
||||
depId: string,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): DependencyInfo {
|
||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||
pkgInstalled,
|
||||
depId,
|
||||
depErrors,
|
||||
)
|
||||
|
||||
const depInfo = pkgInstalled['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkgInstalled.manifest.dependencies[depId].version, // do we want this version range?
|
||||
title: depInfo?.manifest?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText: errorText
|
||||
? `${errorText}. ${pkgInstalled.manifest.title} will not work as expected.`
|
||||
: '',
|
||||
actionText: fixText || 'View',
|
||||
action:
|
||||
fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)),
|
||||
}
|
||||
}
|
||||
|
||||
private getDepErrors(
|
||||
pkgInstalled: InstalledPackageDataEntry,
|
||||
depId: string,
|
||||
depErrors: PackageDependencyErrors,
|
||||
) {
|
||||
const pkgManifest = pkgInstalled.manifest
|
||||
const depError = depErrors[pkgInstalled.manifest.id][depId]
|
||||
|
||||
let errorText: string | null = null
|
||||
let fixText: string | null = null
|
||||
let fixAction: (() => any) | null = null
|
||||
|
||||
if (depError) {
|
||||
if (depError.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
fixText = 'Install'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'install', depId)
|
||||
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'update', depId)
|
||||
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
fixText = 'Auto config'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'configure', depId)
|
||||
} else if (depError.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
} else if (depError.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Health check failed'
|
||||
} else if (depError.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorText,
|
||||
fixText,
|
||||
fixAction,
|
||||
}
|
||||
}
|
||||
|
||||
private async fixDep(
|
||||
pkgManifest: Manifest,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkgManifest, id)
|
||||
case 'configure':
|
||||
return this.configureDep(pkgManifest, id)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
const version = pkgManifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: pkgManifest.id,
|
||||
title: pkgManifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
pkgManifest: Manifest,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: pkgManifest.id,
|
||||
title: pkgManifest.title,
|
||||
}
|
||||
|
||||
await this.modalService.presentModalConfig({
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { DependencyInfo } from '../../app-show.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-dependencies',
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ErrorToastService } from '@start9labs/shared'
|
||||
import { AlertController, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
@@ -32,9 +31,6 @@ export class AppShowStatusComponent {
|
||||
@Input()
|
||||
status!: PackageStatus
|
||||
|
||||
@Input()
|
||||
dependencies: DependencyInfo[] = []
|
||||
|
||||
PR = PrimaryRendering
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
@@ -80,7 +76,7 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.dependencies.some(d => !!d.errorText)) {
|
||||
if (this.status.dependency === 'warning') {
|
||||
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { NavigationExtras } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import {
|
||||
DependencyError,
|
||||
DependencyErrorType,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toDependencies',
|
||||
})
|
||||
export class ToDependenciesPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly modalService: ModalService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): DependencyInfo[] {
|
||||
if (!pkg.installed) return []
|
||||
|
||||
return Object.keys(pkg.installed?.['current-dependencies'])
|
||||
.filter(id => !!pkg.manifest.dependencies[id])
|
||||
.map(id =>
|
||||
this.setDepValues(pkg, id, pkg.installed!.status['dependency-errors']),
|
||||
)
|
||||
}
|
||||
|
||||
private setDepValues(
|
||||
pkg: PackageDataEntry,
|
||||
id: string,
|
||||
errors: { [id: string]: DependencyError | null },
|
||||
): DependencyInfo {
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action: () => any = () =>
|
||||
this.navCtrl.navigateForward(`/services/${id}`)
|
||||
|
||||
const error = errors[id]
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if (
|
||||
[
|
||||
DependencyErrorType.InterfaceHealthChecksFailed,
|
||||
DependencyErrorType.HealthChecksFailed,
|
||||
].includes(error.type)
|
||||
) {
|
||||
errorText = 'Health check failed'
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep(pkg, 'install', id)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep(pkg, 'update', id)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
actionText = 'Start'
|
||||
// config unsatisfied
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep(pkg, 'configure', id)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = pkg.installed?.['dependency-info'][id]
|
||||
|
||||
return {
|
||||
id,
|
||||
version: pkg.manifest.dependencies[id].version,
|
||||
title: depInfo?.manifest?.title || id,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg, id)
|
||||
case 'configure':
|
||||
return this.configureDep(pkg, id)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(
|
||||
pkg: PackageDataEntry,
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
const version = pkg.manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: pkg.manifest.id,
|
||||
title: pkg.manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
pkg: PackageDataEntry,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: pkg.manifest.id,
|
||||
title: pkg.manifest.title,
|
||||
}
|
||||
|
||||
await this.modalService.presentModalConfig({
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'toStatus',
|
||||
})
|
||||
export class ToStatusPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry): PackageStatus {
|
||||
return renderPkgStatus(pkg)
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,10 @@ import {
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { dryUpdate } from 'src/app/util/dry-update'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-controls',
|
||||
@@ -57,7 +56,6 @@ export class MarketplaceShowControlsComponent {
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly emver: Emver,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
@@ -142,30 +140,19 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
|
||||
private async dryInstall(url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Checking dependent services...',
|
||||
})
|
||||
await loader.present()
|
||||
const breakages = dryUpdate(
|
||||
this.pkg.manifest,
|
||||
await getAllPackages(this.patch),
|
||||
this.emver,
|
||||
)
|
||||
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
try {
|
||||
const breakages = await this.embassyApi.dryUpdatePackage({
|
||||
id,
|
||||
version: `${version}`,
|
||||
})
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(url, loader)
|
||||
} else {
|
||||
await loader.dismiss()
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install(url)
|
||||
}
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install(url)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,14 +181,11 @@ export class MarketplaceShowControlsComponent {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async install(url: string, loader?: HTMLIonLoadingElement) {
|
||||
const message = 'Beginning Install...'
|
||||
if (loader) {
|
||||
loader.message = message
|
||||
} else {
|
||||
loader = await this.loadingCtrl.create({ message })
|
||||
await loader.present()
|
||||
}
|
||||
private async install(url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Beginning Install...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
@@ -214,14 +198,10 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
|
||||
private async presentAlertBreakages(breakages: string[]): Promise<boolean> {
|
||||
let message: string =
|
||||
'As a result of this update, the following services will no longer work properly and may crash:<ul>'
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages).map(id => {
|
||||
const title = localPkgs[id].manifest.title
|
||||
return `<li><b>${title}</b></li>`
|
||||
})
|
||||
const bullets = breakages.map(title => `<li><b>${title}</b></li>`)
|
||||
message = `${message}${bullets.join('')}</ul>`
|
||||
|
||||
return new Promise(async resolve => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
@@ -16,14 +15,10 @@ import {
|
||||
import { Emver, isEmptyObject } from '@start9labs/shared'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { AlertController, NavController } from '@ionic/angular'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { dryUpdate } from 'src/app/util/dry-update'
|
||||
|
||||
interface UpdatesData {
|
||||
hosts: StoreIdentity[]
|
||||
@@ -48,11 +43,10 @@ export class UpdatesPage {
|
||||
constructor(
|
||||
@Inject(AbstractMarketplaceService)
|
||||
readonly marketplaceService: MarketplaceService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly emver: Emver,
|
||||
) {}
|
||||
|
||||
viewInMarketplace(event: Event, url: string, id: string) {
|
||||
@@ -77,55 +71,40 @@ export class UpdatesPage {
|
||||
this.marketplaceService.updateQueue[id] = true
|
||||
|
||||
if (hasCurrentDeps(local)) {
|
||||
this.dryUpdate(manifest, url)
|
||||
this.dryInstall(manifest, url)
|
||||
} else {
|
||||
this.update(id, version, url)
|
||||
this.install(id, version, url)
|
||||
}
|
||||
}
|
||||
|
||||
private async dryUpdate(manifest: MarketplaceManifest, url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Checking dependent services...',
|
||||
})
|
||||
await loader.present()
|
||||
private async dryInstall(manifest: MarketplaceManifest, url: string) {
|
||||
const { id, version, title } = manifest
|
||||
|
||||
const { id, version } = manifest
|
||||
const breakages = dryUpdate(
|
||||
manifest,
|
||||
await getAllPackages(this.patch),
|
||||
this.emver,
|
||||
)
|
||||
|
||||
try {
|
||||
const breakages = await this.api.dryUpdatePackage({
|
||||
id,
|
||||
version: `${version}`,
|
||||
})
|
||||
await loader.dismiss()
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.update(id, version, url)
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(id, version, url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(title, breakages)
|
||||
if (proceed) {
|
||||
this.install(id, version, url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(
|
||||
manifest.title,
|
||||
breakages,
|
||||
)
|
||||
if (proceed) {
|
||||
this.update(id, version, url)
|
||||
} else {
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
}
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
}
|
||||
} catch (e: any) {
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
this.marketplaceService.updateErrors[id] = e.message
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertBreakages(
|
||||
title: string,
|
||||
breakages: Breakages,
|
||||
breakages: string[],
|
||||
): Promise<boolean> {
|
||||
let message: string = `As a result of updating ${title}, the following services will no longer work properly and may crash:<ul>`
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages).map(id => {
|
||||
const title = localPkgs[id].manifest.title
|
||||
return `<li><b>${title}</b></li>`
|
||||
const bullets = breakages.map(depTitle => {
|
||||
return `<li><b>${depTitle}</b></li>`
|
||||
})
|
||||
message = `${message}${bullets.join('')}</ul>`
|
||||
|
||||
@@ -156,7 +135,7 @@ export class UpdatesPage {
|
||||
})
|
||||
}
|
||||
|
||||
private async update(id: string, version: string, url: string) {
|
||||
private async install(id: string, version: string, url: string) {
|
||||
try {
|
||||
await this.marketplaceService.installPackage(id, version, url)
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} 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'
|
||||
|
||||
@@ -20,11 +23,13 @@ export class HealthComponent {
|
||||
'Transitioning',
|
||||
] as const
|
||||
|
||||
readonly data$ = inject(PatchDB)
|
||||
readonly data$ = inject(PatchDB<DataModel>)
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(data => {
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(getPackageInfo)
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(
|
||||
pkg => getPackageInfo(pkg, {}), // @TODO hack because not currently using widget
|
||||
)
|
||||
const result = this.labels.reduce<Record<string, number>>(
|
||||
(acc, label) => ({
|
||||
...acc,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
DependencyErrorType,
|
||||
DockerIoFormat,
|
||||
Manifest,
|
||||
PackageDataEntry,
|
||||
@@ -1889,7 +1888,7 @@ export module Mock {
|
||||
started: new Date().toISOString(),
|
||||
health: {},
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
'interface-addresses': {
|
||||
ui: {
|
||||
@@ -1935,7 +1934,7 @@ export module Mock {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
manifest: MockManifestBitcoinProxy,
|
||||
'interface-addresses': {
|
||||
@@ -1984,10 +1983,8 @@ export module Mock {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {
|
||||
'btc-rpc-proxy': {
|
||||
type: DependencyErrorType.NotInstalled,
|
||||
},
|
||||
'dependency-config-errors': {
|
||||
'btc-rpc-proxy': 'Username not found',
|
||||
},
|
||||
},
|
||||
manifest: MockManifestLnd,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyError,
|
||||
HealthCheckResult,
|
||||
Manifest,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
@@ -201,9 +201,6 @@ export module RR {
|
||||
} // package.install
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type DryUpdatePackageReq = { id: string; version: string } // package.update.dry
|
||||
export type DryUpdatePackageRes = Breakages
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: ConfigSpec; config: object }
|
||||
|
||||
@@ -465,3 +462,53 @@ declare global {
|
||||
parse<T>(text: Stringified<T>, reviver?: (key: any, value: any) => any): T
|
||||
}
|
||||
}
|
||||
|
||||
export type Encrypted = {
|
||||
encrypted: string
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
@@ -192,10 +192,6 @@ export abstract class ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes>
|
||||
|
||||
abstract dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes>
|
||||
|
||||
abstract getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes>
|
||||
|
||||
@@ -354,12 +354,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyErrorType,
|
||||
InstallProgress,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
@@ -631,22 +630,6 @@ export class MockApiService extends ApiService {
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
lnd: {
|
||||
dependency: 'bitcoind',
|
||||
error: {
|
||||
type: DependencyErrorType.IncorrectVersion,
|
||||
expected: '>0.23.0',
|
||||
received: params.version,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
DataModel,
|
||||
DependencyErrorType,
|
||||
DockerIoFormat,
|
||||
HealthResult,
|
||||
Manifest,
|
||||
@@ -438,7 +437,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
'interface-addresses': {
|
||||
ui: {
|
||||
@@ -637,11 +636,8 @@ export const mockPatchData: DataModel = {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {
|
||||
'btc-rpc-proxy': {
|
||||
type: DependencyErrorType.ConfigUnsatisfied,
|
||||
error: 'This is a config unsatisfied error',
|
||||
},
|
||||
'dependency-config-errors': {
|
||||
'btc-rpc-proxy': 'This is a config unsatisfied error',
|
||||
},
|
||||
},
|
||||
'interface-addresses': {
|
||||
|
||||
211
frontend/projects/ui/src/app/services/dep-error.service.ts
Normal file
211
frontend/projects/ui/src/app/services/dep-error.service.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { map, shareReplay } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
HealthResult,
|
||||
InstalledPackageDataEntry,
|
||||
PackageMainStatus,
|
||||
} from './patch-db/data-model'
|
||||
|
||||
export type PackageDependencyErrors = Record<string, DependencyErrors>
|
||||
export type DependencyErrors = Record<string, DependencyError | null>
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DepErrorService {
|
||||
readonly depErrors$ = this.patch.watch$('package-data').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 }): PackageDependencyErrors => ({
|
||||
...errors,
|
||||
[id]: this.getDepErrors(pkgs, id, errors),
|
||||
}),
|
||||
{} as PackageDependencyErrors,
|
||||
),
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
private getDepErrors(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgId: string,
|
||||
outerErrors: PackageDependencyErrors,
|
||||
): DependencyErrors {
|
||||
const pkgInstalled = pkgs[pkgId].installed
|
||||
|
||||
if (!pkgInstalled) return {}
|
||||
|
||||
return currentDeps(pkgs, pkgId).reduce(
|
||||
(innerErrors, depId): DependencyErrors => ({
|
||||
...innerErrors,
|
||||
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
|
||||
}),
|
||||
{} as DependencyErrors,
|
||||
)
|
||||
}
|
||||
|
||||
private getDepError(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgInstalled: InstalledPackageDataEntry,
|
||||
depId: string,
|
||||
outerErrors: PackageDependencyErrors,
|
||||
): DependencyError | null {
|
||||
const depInstalled = pkgs[depId]?.installed
|
||||
|
||||
// not installed
|
||||
if (!depInstalled) {
|
||||
return {
|
||||
type: DependencyErrorType.NotInstalled,
|
||||
}
|
||||
}
|
||||
|
||||
const pkgManifest = pkgInstalled.manifest
|
||||
const depManifest = depInstalled.manifest
|
||||
|
||||
// incorrect version
|
||||
if (
|
||||
!this.emver.satisfies(
|
||||
depManifest.version,
|
||||
pkgManifest.dependencies[depId].version,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.IncorrectVersion,
|
||||
expected: pkgManifest.dependencies[depId].version,
|
||||
received: depManifest.version,
|
||||
}
|
||||
}
|
||||
|
||||
// invalid config
|
||||
if (
|
||||
Object.values(pkgInstalled.status['dependency-config-errors']).some(
|
||||
err => !!err,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.ConfigUnsatisfied,
|
||||
}
|
||||
}
|
||||
|
||||
const depStatus = depInstalled.status.main.status
|
||||
|
||||
// not running
|
||||
if (
|
||||
depStatus !== PackageMainStatus.Running &&
|
||||
depStatus !== PackageMainStatus.Starting &&
|
||||
!(
|
||||
depStatus === PackageMainStatus.BackingUp &&
|
||||
depInstalled.status.main.started
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.NotRunning,
|
||||
}
|
||||
}
|
||||
|
||||
// health check failure
|
||||
if (depStatus === PackageMainStatus.Running) {
|
||||
for (let id of pkgInstalled['current-dependencies'][depId][
|
||||
'health-checks'
|
||||
]) {
|
||||
if (
|
||||
depInstalled.status.main.health[id].result !== HealthResult.Success
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.HealthChecksFailed,
|
||||
check: depInstalled.status.main.health[id],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transitive
|
||||
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
|
||||
Object.values(outerErrors[transitiveId]).some(err => !!err),
|
||||
)
|
||||
|
||||
if (transitiveError) {
|
||||
return {
|
||||
type: DependencyErrorType.Transitive,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
|
||||
return Object.keys(
|
||||
pkgs[id]?.installed?.['current-dependencies'] || {},
|
||||
).filter(depId => depId !== id)
|
||||
}
|
||||
|
||||
function dependencyDepth(
|
||||
pkgs: DataModel['package-data'],
|
||||
id: string,
|
||||
depth = 0,
|
||||
): number {
|
||||
return currentDeps(pkgs, id).reduce(
|
||||
(prev, depId) => dependencyDepth(pkgs, depId, prev + 1),
|
||||
depth,
|
||||
)
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'notInstalled',
|
||||
NotRunning = 'notRunning',
|
||||
IncorrectVersion = 'incorrectVersion',
|
||||
ConfigUnsatisfied = 'configUnsatisfied',
|
||||
HealthChecksFailed = 'healthChecksFailed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
@@ -277,7 +277,7 @@ export interface Action {
|
||||
export interface Status {
|
||||
configured: boolean
|
||||
main: MainStatus
|
||||
'dependency-errors': { [id: string]: DependencyError | null }
|
||||
'dependency-config-errors': { [id: string]: string | null }
|
||||
}
|
||||
|
||||
export type MainStatus =
|
||||
@@ -362,52 +362,6 @@ export interface HealthCheckResultFailure {
|
||||
error: string
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
export interface InstallProgress {
|
||||
readonly size: number | null
|
||||
readonly downloaded: number
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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'
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus
|
||||
@@ -13,16 +15,21 @@ export interface PackageStatus {
|
||||
health: HealthStatus | null
|
||||
}
|
||||
|
||||
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
export function renderPkgStatus(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): PackageStatus {
|
||||
let primary: PrimaryStatus
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
const hasHealthChecks = !isEmptyObject(pkg.manifest['health-checks'])
|
||||
|
||||
if (pkg.state === PackageState.Installed && pkg.installed) {
|
||||
primary = getPrimaryStatus(pkg.installed.status)
|
||||
dependency = getDependencyStatus(pkg)
|
||||
health = getHealthStatus(pkg.installed.status, hasHealthChecks)
|
||||
dependency = getDependencyStatus(pkg.installed, depErrors)
|
||||
health = getHealthStatus(
|
||||
pkg.installed.status,
|
||||
!isEmptyObject(pkg.manifest['health-checks']),
|
||||
)
|
||||
} else {
|
||||
primary = pkg.state as string as PrimaryStatus
|
||||
}
|
||||
@@ -40,15 +47,13 @@ function getPrimaryStatus(status: Status): PrimaryStatus {
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
|
||||
const installed = pkg.installed
|
||||
if (!installed || isEmptyObject(installed['current-dependencies']))
|
||||
return null
|
||||
|
||||
const depErrors = installed.status['dependency-errors']
|
||||
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
|
||||
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
function getDependencyStatus(
|
||||
pkg: InstalledPackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): DependencyStatus {
|
||||
return Object.values(depErrors[pkg.manifest.id]).some(err => !!err)
|
||||
? DependencyStatus.Warning
|
||||
: DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus(
|
||||
|
||||
17
frontend/projects/ui/src/app/util/dry-update.ts
Normal file
17
frontend/projects/ui/src/app/util/dry-update.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { DataModel } from '../services/patch-db/data-model'
|
||||
|
||||
export function dryUpdate(
|
||||
{ id, version }: { id: string; version: string },
|
||||
pkgs: DataModel['package-data'],
|
||||
emver: Emver,
|
||||
): string[] {
|
||||
return Object.values(pkgs)
|
||||
.filter(
|
||||
pkg =>
|
||||
Object.keys(pkg.installed?.['current-dependencies'] || {}).some(
|
||||
pkgId => pkgId === id,
|
||||
) && !emver.satisfies(version, pkg.manifest.dependencies[id].version),
|
||||
)
|
||||
.map(pkg => pkg.manifest.title)
|
||||
}
|
||||
@@ -14,6 +14,6 @@ export async function getPackage(
|
||||
|
||||
export async function getAllPackages(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<Record<string, PackageDataEntry>> {
|
||||
): Promise<DataModel['package-data']> {
|
||||
return firstValueFrom(patch.watch$('package-data'))
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@ 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'
|
||||
|
||||
export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry)
|
||||
export function getPackageInfo(
|
||||
entry: PackageDataEntry,
|
||||
depErrors: PackageDependencyErrors,
|
||||
): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry, depErrors)
|
||||
const primaryRendering = PrimaryRendering[statuses.primary]
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user