Rework PackageDataEntry for new strategy (#2573)

* rework PackageDataEntry for new strategy

* fix type error

* fix issues with manifest fetching

* mock installs working
This commit is contained in:
Matt Hill
2024-03-19 08:38:04 -06:00
committed by GitHub
parent c8be701f0e
commit cc38dab76f
64 changed files with 1759 additions and 2068 deletions

2
web/package-lock.json generated
View File

@@ -1973,7 +1973,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-rev0.lib0.rc8.beta7", "version": "0.4.0-rev0.lib0.rc8.beta10",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",

View File

@@ -20,11 +20,18 @@ import {
} from 'rxjs' } from 'rxjs'
import { AbstractMarketplaceService } from '@start9labs/marketplace' import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
import { SplitPaneTracker } from 'src/app/services/split-pane.service' import { SplitPaneTracker } from 'src/app/services/split-pane.service'
import { Emver, THEME } from '@start9labs/shared' import { Emver, THEME } from '@start9labs/shared'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import {
getManifest,
isInstalled,
isInstalling,
isRestoring,
isUpdating,
} from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'app-menu', selector: 'app-menu',
@@ -79,8 +86,14 @@ export class MenuComponent {
filter(([prev, curr]) => filter(([prev, curr]) =>
Object.values(prev).some( Object.values(prev).some(
p => p =>
p['install-progress'] && [
!curr[p.manifest.id]?.['install-progress'], PackageState.Installing,
PackageState.Updating,
PackageState.Restoring,
].includes(p['state-info'].state) &&
[PackageState.Installed, PackageState.Removing].includes(
curr[getManifest(p).id]['state-info'].state,
),
), ),
), ),
map(([_, curr]) => curr), map(([_, curr]) => curr),
@@ -97,9 +110,10 @@ export class MenuComponent {
Object.entries(marketplace).reduce((list, [_, store]) => { Object.entries(marketplace).reduce((list, [_, store]) => {
store?.packages.forEach(({ manifest: { id, version } }) => { store?.packages.forEach(({ manifest: { id, version } }) => {
if ( if (
local[id] &&
this.emver.compare( this.emver.compare(
version, version,
local[id]?.installed?.manifest.version || '', getManifest(local[id]).version || '',
) === 1 ) === 1
) )
list.add(id) list.add(id)

View File

@@ -12,12 +12,9 @@
. This may take a while . This may take a while
</span> </span>
<span *ngIf="installProgress"> <span *ngIf="installingInfo">
<ion-text <ion-text color="primary">
*ngIf="installProgress | installProgressDisplay as progress" {{ installingInfo.progress.overall | installingProgressString }}
color="primary"
>
{{ progress }}
</ion-text> </ion-text>
</span> </span>

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { UnitConversionPipesModule } from '@start9labs/shared' import { UnitConversionPipesModule } from '@start9labs/shared'
import { StatusComponent } from './status.component' import { StatusComponent } from './status.component'
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
@NgModule({ @NgModule({
declarations: [StatusComponent], declarations: [StatusComponent],
@@ -11,7 +11,7 @@ import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/instal
CommonModule, CommonModule,
IonicModule, IonicModule,
UnitConversionPipesModule, UnitConversionPipesModule,
InstallProgressPipeModule, InstallingProgressPipeModule,
], ],
exports: [StatusComponent], exports: [StatusComponent],
}) })

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { InstallProgress } from 'src/app/services/patch-db/data-model' import { InstallingInfo } from 'src/app/services/patch-db/data-model'
import { import {
PrimaryRendering, PrimaryRendering,
PrimaryStatus, PrimaryStatus,
@@ -20,7 +20,7 @@ export class StatusComponent {
@Input() size?: string @Input() size?: string
@Input() style?: string = 'regular' @Input() style?: string = 'regular'
@Input() weight?: string = 'normal' @Input() weight?: string = 'normal'
@Input() installProgress?: InstallProgress @Input() installingInfo?: InstallingInfo
@Input() sigtermTimeout?: string | null = null @Input() sigtermTimeout?: string | null = null
readonly connected$ = this.connectionService.connected$ readonly connected$ = this.connectionService.connected$

View File

@@ -9,7 +9,7 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding" *ngIf="pkg['state-info'].manifest as manifest">
<!-- loading --> <!-- loading -->
<text-spinner <text-spinner
*ngIf="loading; else notLoading" *ngIf="loading; else notLoading"
@@ -25,14 +25,14 @@
</ion-item> </ion-item>
<ng-template #noError> <ng-template #noError>
<ng-container *ngIf="configForm && !pkg.installed?.status?.configured"> <ng-container *ngIf="configForm && !pkg.status.configured">
<ng-container *ngIf="!original; else hasOriginal"> <ng-container *ngIf="!original; else hasOriginal">
<h2 <h2
*ngIf="!configForm.dirty" *ngIf="!configForm.dirty"
class="ion-padding-bottom header-details" class="ion-padding-bottom header-details"
> >
<ion-text color="success"> <ion-text color="success">
{{ pkg.manifest.title }} has been automatically configured with {{ manifest.title }} has been automatically configured with
recommended defaults. Make whatever changes you want, then click recommended defaults. Make whatever changes you want, then click
"Save". "Save".
</ion-text> </ion-text>
@@ -59,19 +59,19 @@
<h2 style="display: flex; align-items: center"> <h2 style="display: flex; align-items: center">
<img <img
style="width: 18px; margin: 4px" style="width: 18px; margin: 4px"
[src]="pkg['static-files'].icon" [src]="pkg.icon"
[alt]="pkg.manifest.title" [alt]="manifest.title"
/> />
<ion-text <ion-text
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px" style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
> >
{{ pkg.manifest.title }} {{ manifest.title }}
</ion-text> </ion-text>
</h2> </h2>
<p> <p>
<ion-text color="dark"> <ion-text color="dark">
The following modifications have been made to {{ The following modifications have been made to {{ manifest.title }}
pkg.manifest.title }} to satisfy {{ dependentInfo.title }}: to satisfy {{ dependentInfo.title }}:
<ul> <ul>
<li *ngFor="let d of diff" [innerHtml]="d"></li> <li *ngFor="let d of diff" [innerHtml]="d"></li>
</ul> </ul>
@@ -85,8 +85,7 @@
<ion-item *ngIf="!hasOptions"> <ion-item *ngIf="!hasOptions">
<ion-label> <ion-label>
<p> <p>
No config options for {{ pkg.manifest.title }} {{ No config options for {{ manifest.title }} {{ manifest.version }}.
pkg.manifest.version }}.
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@@ -16,6 +16,7 @@ import { DependentInfo } from 'src/app/types/dependent-info'
import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { import {
DataModel, DataModel,
InstalledState,
PackageDataEntry, PackageDataEntry,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
@@ -26,7 +27,12 @@ import {
} from 'src/app/services/form.service' } from 'src/app/services/form.service'
import { compare, Operation, getValueByPointer } from 'fast-json-patch' import { compare, Operation, getValueByPointer } from 'fast-json-patch'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getAllPackages, getPackage } from 'src/app/util/get-package-data' import {
getAllPackages,
getManifest,
getPackage,
isInstalled,
} from 'src/app/util/get-package-data'
import { Breakages } from 'src/app/services/api/api.types' import { Breakages } from 'src/app/services/api/api.types'
@Component({ @Component({
@@ -39,7 +45,7 @@ export class AppConfigPage {
@Input() dependentInfo?: DependentInfo @Input() dependentInfo?: DependentInfo
pkg!: PackageDataEntry pkg!: PackageDataEntry<InstalledState>
loadingText = '' loadingText = ''
configSpec?: ConfigSpec configSpec?: ConfigSpec
@@ -68,10 +74,11 @@ export class AppConfigPage {
async ngOnInit() { async ngOnInit() {
try { try {
const pkg = await getPackage(this.patch, this.pkgId) const pkg = await getPackage(this.patch, this.pkgId)
if (!pkg) return if (!pkg || !isInstalled(pkg)) return
this.pkg = pkg this.pkg = pkg
if (!this.pkg.manifest.config) return if (!this.pkg['state-info'].manifest.config) return
let newConfig: object | undefined let newConfig: object | undefined
let patch: Operation[] | undefined let patch: Operation[] | undefined
@@ -210,7 +217,7 @@ export class AppConfigPage {
'As a result of this change, the following services will no longer work properly and may crash:<ul>' 'As a result of this change, the following services will no longer work properly and may crash:<ul>'
const localPkgs = await getAllPackages(this.patch) const localPkgs = await getAllPackages(this.patch)
const bullets = Object.keys(breakages).map(id => { const bullets = Object.keys(breakages).map(id => {
const title = localPkgs[id].manifest.title const title = getManifest(localPkgs[id]).title
return `<li><b>${title}</b></li>` return `<li><b>${title}</b></li>`
}) })
message = `${message}${bullets}</ul>` message = `${message}${bullets}</ul>`

View File

@@ -4,6 +4,7 @@ import { map, take } from 'rxjs/operators'
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model' import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs' import { firstValueFrom } from 'rxjs'
import { getManifest } from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'backup-select', selector: 'backup-select',
@@ -32,13 +33,13 @@ export class BackupSelectPage {
map(pkgs => { map(pkgs => {
return Object.values(pkgs) return Object.values(pkgs)
.map(pkg => { .map(pkg => {
const { id, title } = pkg.manifest const { id, title } = getManifest(pkg)
return { return {
id, id,
title, title,
icon: pkg['static-files'].icon, icon: pkg.icon,
disabled: pkg.state !== PackageState.Installed, disabled: pkg['state-info'].state !== PackageState.Installed,
checked: pkg.state === PackageState.Installed, checked: false,
} }
}) })
.sort((a, b) => .sort((a, b) =>

View File

@@ -8,30 +8,34 @@
</ion-header> </ion-header>
<ion-content class="ion-padding-top with-widgets"> <ion-content class="ion-padding-top with-widgets">
<ion-item-group *ngIf="pkg$ | async as pkg"> <ng-container *ngIf="pkg$ | async as pkg">
<!-- ** standard actions ** --> <ion-item-group
<ion-item-divider>Standard Actions</ion-item-divider> *ngIf="pkg['state-info'].state === 'installed' && pkg['state-info'].manifest as manifest"
<app-actions-item >
[action]="{ <!-- ** standard actions ** -->
name: 'Uninstall', <ion-item-divider>Standard Actions</ion-item-divider>
description: 'This will uninstall the service from StartOS and delete all data permanently.', <app-actions-item
icon: 'trash-outline' [action]="{
}" name: 'Uninstall',
(click)="tryUninstall(pkg)" description: 'This will uninstall the service from StartOS and delete all data permanently.',
></app-actions-item> icon: 'trash-outline'
}"
(click)="tryUninstall(pkg)"
></app-actions-item>
<!-- ** specific actions ** --> <!-- ** specific actions ** -->
<ion-item-divider *ngIf="!(pkg.manifest.actions | empty)"> <ion-item-divider *ngIf="!(manifest.actions | empty)">
Actions for {{ pkg.manifest.title }} Actions for {{ manifest.title }}
</ion-item-divider> </ion-item-divider>
<app-actions-item <app-actions-item
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" *ngFor="let action of manifest.actions | keyvalue: asIsOrder"
[action]="{ [action]="{
name: action.value.name, name: action.value.name,
description: action.value.description, description: action.value.description,
icon: 'play-circle-outline' icon: 'play-circle-outline'
}" }"
(click)="handleAction(pkg, action)" (click)="handleAction(pkg.status, action)"
></app-actions-item> ></app-actions-item>
</ion-item-group> </ion-item-group>
</ng-container>
</ion-content> </ion-content>

View File

@@ -11,13 +11,17 @@ import { PatchDB } from 'patch-db-client'
import { import {
Action, Action,
DataModel, DataModel,
InstalledState,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
StateInfo,
Status,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getManifest } from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'app-actions', selector: 'app-actions',
@@ -40,11 +44,7 @@ export class AppActionsPage {
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
async handleAction( async handleAction(status: Status, action: { key: string; value: Action }) {
pkg: PackageDataEntry,
action: { key: string; value: Action },
) {
const status = pkg.installed?.status
if ( if (
status && status &&
(action.value['allowed-statuses'] as PackageMainStatus[]).includes( (action.value['allowed-statuses'] as PackageMainStatus[]).includes(
@@ -120,7 +120,7 @@ export class AppActionsPage {
} }
async tryUninstall(pkg: PackageDataEntry): Promise<void> { async tryUninstall(pkg: PackageDataEntry): Promise<void> {
const { title, alerts } = pkg.manifest const { title, alerts } = getManifest(pkg)
let message = let message =
alerts.uninstall || alerts.uninstall ||

View File

@@ -29,7 +29,7 @@ export class AppInterfacesPage {
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
readonly serviceInterfaces$ = this.patch readonly serviceInterfaces$ = this.patch
.watch$('package-data', this.pkgId, 'installed', 'service-interfaces') .watch$('package-data', this.pkgId, 'service-interfaces')
.pipe( .pipe(
map(interfaces => { map(interfaces => {
const sorted = Object.values(interfaces) const sorted = Object.values(interfaces)

View File

@@ -1,34 +1,34 @@
<ion-item <ion-item
button button
*ngIf="pkg.entry.manifest as manifest" *ngIf="pkg.entry | toManifest as manifest"
detail="false" detail="false"
class="service-card" class="service-card"
[routerLink]="['/services', manifest.id]" [routerLink]="['/services', manifest.id]"
> >
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon> <app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
<ion-thumbnail slot="start"> <ion-thumbnail slot="start">
<img alt="" [src]="pkg.entry['static-files'].icon" /> <img alt="" [src]="pkg.entry.icon" />
</ion-thumbnail> </ion-thumbnail>
<ion-label> <ion-label>
<h2 ticker>{{ manifest.title }}</h2> <h2 ticker>{{ manifest.title }}</h2>
<p>{{ manifest.version | displayEmver }}</p> <p>{{ manifest.version | displayEmver }}</p>
<status <status
[rendering]="pkg.primaryRendering" [rendering]="pkg.primaryRendering"
[installProgress]="pkg.entry['install-progress']" [installingInfo]="$any(pkg.entry['state-info'])['installing-info']"
weight="bold" weight="bold"
size="small" size="small"
[sigtermTimeout]="sigtermTimeout" [sigtermTimeout]="sigtermTimeout"
></status> ></status>
</ion-label> </ion-label>
<ion-button <ion-button
*ngIf=" *ngIf="pkg.entry['service-interfaces'] | hasUi"
pkg.entry.installed && (pkg.entry.installed['service-interfaces'] | hasUi)
"
slot="end" slot="end"
fill="clear" fill="clear"
color="primary" color="primary"
(click)="launchUi($event, pkg.entry.installed['service-interfaces'])" (click)="launchUi($event, pkg.entry['service-interfaces'])"
[disabled]="!(pkg.entry.state | isLaunchable: pkgMainStatus.status)" [disabled]="
!(pkg.entry['state-info'].state | isLaunchable: pkgMainStatus.status)
"
> >
<ion-icon slot="icon-only" name="open-outline"></ion-icon> <ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button> </ion-button>

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { import {
InstalledPackageDataEntry,
MainStatus, MainStatus,
PackageDataEntry,
PackageMainStatus, PackageMainStatus,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { PkgInfo } from 'src/app/util/get-package-info' import { PkgInfo } from 'src/app/util/get-package-info'
@@ -20,7 +20,7 @@ export class AppListPkgComponent {
get pkgMainStatus(): MainStatus { get pkgMainStatus(): MainStatus {
return ( return (
this.pkg.entry.installed?.status.main || { this.pkg.entry.status.main || {
status: PackageMainStatus.Stopped, status: PackageMainStatus.Stopped,
} }
) )
@@ -32,10 +32,7 @@ export class AppListPkgComponent {
: null : null
} }
launchUi( launchUi(e: Event, interfaces: PackageDataEntry['service-interfaces']): void {
e: Event,
interfaces: InstalledPackageDataEntry['service-interfaces'],
): void {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
this.launcherService.launch(interfaces) this.launcherService.launch(interfaces)

View File

@@ -27,7 +27,7 @@
sizeMd="6" sizeMd="6"
> >
<app-list-pkg <app-list-pkg
*ngIf="pkg.manifest.id | packageInfo | async as info" *ngIf="(pkg | toManifest).id | packageInfo | async as info"
[pkg]="info" [pkg]="info"
></app-list-pkg> ></app-list-pkg>
</ion-col> </ion-col>

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { filter, map, pairwise, startWith } from 'rxjs/operators' import { filter, map, pairwise, startWith } from 'rxjs/operators'
import { getManifest } from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'app-list', selector: 'app-list',
@@ -20,7 +21,7 @@ export class AppListPage {
}), }),
map(([_, pkgs]) => map(([_, pkgs]) =>
pkgs.sort((a, b) => pkgs.sort((a, b) =>
b.manifest.title.toLowerCase() > a.manifest.title.toLowerCase() getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
? -1 ? -1
: 1, : 1,
), ),

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { Observable, combineLatest, firstValueFrom } from 'rxjs' import { Observable, combineLatest } from 'rxjs'
import { filter, map } from 'rxjs/operators' import { filter, map } from 'rxjs/operators'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { getPackageInfo, PkgInfo } from '../../../util/get-package-info' import { getPackageInfo, PkgInfo } from '../../../util/get-package-info'

View File

@@ -41,7 +41,7 @@ export class AppPropertiesPage {
unmasked: { [key: string]: boolean } = {} unmasked: { [key: string]: boolean } = {}
stopped$ = this.patch stopped$ = this.patch
.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status') .watch$('package-data', this.pkgId, 'status', 'main', 'status')
.pipe(map(status => status === PackageMainStatus.Stopped)) .pipe(map(status => status === PackageMainStatus.Stopped))
@ViewChild(IonBackButtonDelegate, { static: false }) @ViewChild(IonBackButtonDelegate, { static: false })

View File

@@ -18,7 +18,7 @@ import { AppShowAdditionalComponent } from './components/app-show-additional/app
import { HealthColorPipe } from './pipes/health-color.pipe' import { HealthColorPipe } from './pipes/health-color.pipe'
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe' import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
import { ToButtonsPipe } from './pipes/to-buttons.pipe' import { ToButtonsPipe } from './pipes/to-buttons.pipe'
import { ProgressDataPipe } from './pipes/progress-data.pipe' import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -31,7 +31,6 @@ const routes: Routes = [
declarations: [ declarations: [
AppShowPage, AppShowPage,
HealthColorPipe, HealthColorPipe,
ProgressDataPipe,
ToHealthChecksPipe, ToHealthChecksPipe,
ToButtonsPipe, ToButtonsPipe,
AppShowHeaderComponent, AppShowHeaderComponent,
@@ -44,7 +43,7 @@ const routes: Routes = [
], ],
imports: [ imports: [
CommonModule, CommonModule,
StatusComponentModule, InstallingProgressPipeModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),
AppConfigPageModule, AppConfigPageModule,
@@ -52,6 +51,7 @@ const routes: Routes = [
LaunchablePipeModule, LaunchablePipeModule,
UiPipeModule, UiPipeModule,
ResponsiveColModule, ResponsiveColModule,
StatusComponentModule,
], ],
}) })
export class AppShowPageModule {} export class AppShowPageModule {}

View File

@@ -7,9 +7,8 @@
<!-- ** installing, updating, restoring ** --> <!-- ** installing, updating, restoring ** -->
<ng-container *ngIf="showProgress(pkg); else installed"> <ng-container *ngIf="showProgress(pkg); else installed">
<app-show-progress <app-show-progress
*ngIf="pkg | progressData as progressData" *ngIf="pkg['state-info']['installing-info'] as installingInfo"
[pkg]="pkg" [phases]="installingInfo.progress.phases"
[progressData]="progressData"
></app-show-progress> ></app-show-progress>
</ng-container> </ng-container>
@@ -19,11 +18,13 @@
<!-- ** status ** --> <!-- ** status ** -->
<app-show-status [pkg]="pkg" [status]="status"></app-show-status> <app-show-status [pkg]="pkg" [status]="status"></app-show-status>
<!-- ** installed && !backing-up ** --> <!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)"> <ng-container
*ngIf="isInstalled(pkg) && status.primary !== 'backing-up'"
>
<!-- ** health checks ** --> <!-- ** health checks ** -->
<app-show-health-checks <app-show-health-checks
*ngIf="isRunning(status)" *ngIf="status.primary === 'running'"
[pkg]="pkg" [manifest]="pkg['state-info'].manifest"
></app-show-health-checks> ></app-show-health-checks>
<!-- ** dependencies ** --> <!-- ** dependencies ** -->
<app-show-dependencies <app-show-dependencies
@@ -33,7 +34,9 @@
<!-- ** menu ** --> <!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu> <app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** --> <!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional> <app-show-additional
[manifest]="pkg['state-info'].manifest"
></app-show-additional>
</ng-container> </ng-container>
</ion-item-group> </ion-item-group>
</ng-template> </ng-template>

View File

@@ -3,16 +3,12 @@ import { NavController } from '@ionic/angular'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
DataModel, DataModel,
InstalledPackageDataEntry, InstallingState,
Manifest, Manifest,
PackageDataEntry, PackageDataEntry,
PackageState, UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
PackageStatus,
PrimaryStatus,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
import { map, tap } from 'rxjs/operators' import { map, tap } from 'rxjs/operators'
import { ActivatedRoute, NavigationExtras } from '@angular/router' import { ActivatedRoute, NavigationExtras } from '@angular/router'
import { getPkgId } from '@start9labs/shared' import { getPkgId } from '@start9labs/shared'
@@ -24,6 +20,13 @@ import {
PkgDependencyErrors, PkgDependencyErrors,
} from 'src/app/services/dep-error.service' } from 'src/app/services/dep-error.service'
import { combineLatest } from 'rxjs' import { combineLatest } from 'rxjs'
import {
getManifest,
isInstalled,
isInstalling,
isRestoring,
isUpdating,
} from 'src/app/util/get-package-data'
export interface DependencyInfo { export interface DependencyInfo {
id: string id: string
@@ -35,12 +38,6 @@ export interface DependencyInfo {
action: () => any action: () => any
} }
const STATES = [
PackageState.Installing,
PackageState.Updating,
PackageState.Restoring,
]
@Component({ @Component({
selector: 'app-show', selector: 'app-show',
templateUrl: './app-show.page.html', templateUrl: './app-show.page.html',
@@ -66,6 +63,8 @@ export class AppShowPage {
}), }),
) )
isInstalled = isInstalled
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
@@ -74,55 +73,44 @@ export class AppShowPage {
private readonly depErrorService: DepErrorService, private readonly depErrorService: DepErrorService,
) {} ) {}
isInstalled({ state }: PackageDataEntry): boolean { showProgress(
return state === PackageState.Installed pkg: PackageDataEntry,
} ): pkg is PackageDataEntry<InstallingState | UpdatingState> {
return isInstalling(pkg) || isUpdating(pkg) || isRestoring(pkg)
isRunning({ primary }: PackageStatus): boolean {
return primary === PrimaryStatus.Running
}
isBackingUp({ primary }: PackageStatus): boolean {
return primary === PrimaryStatus.BackingUp
}
showProgress({ state }: PackageDataEntry): boolean {
return STATES.includes(state)
} }
private getDepInfo( private getDepInfo(
pkg: PackageDataEntry, pkg: PackageDataEntry,
depErrors: PkgDependencyErrors, depErrors: PkgDependencyErrors,
): DependencyInfo[] { ): DependencyInfo[] {
const pkgInstalled = pkg.installed const manifest = getManifest(pkg)
if (!pkgInstalled) return [] return Object.keys(pkg['current-dependencies'])
.filter(id => !!manifest.dependencies[id])
return Object.keys(pkgInstalled['current-dependencies']) .map(id => this.getDepValues(pkg, manifest, id, depErrors))
.filter(id => !!pkgInstalled.manifest.dependencies[id])
.map(id => this.getDepValues(pkgInstalled, id, depErrors))
} }
private getDepValues( private getDepValues(
pkgInstalled: InstalledPackageDataEntry, pkg: PackageDataEntry,
manifest: Manifest,
depId: string, depId: string,
depErrors: PkgDependencyErrors, depErrors: PkgDependencyErrors,
): DependencyInfo { ): DependencyInfo {
const { errorText, fixText, fixAction } = this.getDepErrors( const { errorText, fixText, fixAction } = this.getDepErrors(
pkgInstalled, manifest,
depId, depId,
depErrors, depErrors,
) )
const depInfo = pkgInstalled['dependency-info'][depId] const depInfo = pkg['dependency-info'][depId]
return { return {
id: depId, id: depId,
version: pkgInstalled.manifest.dependencies[depId].version, // do we want this version range? version: manifest.dependencies[depId].version, // do we want this version range?
title: depInfo?.title || depId, title: depInfo?.title || depId,
icon: depInfo?.icon || '', icon: depInfo?.icon || '',
errorText: errorText errorText: errorText
? `${errorText}. ${pkgInstalled.manifest.title} will not work as expected.` ? `${errorText}. ${manifest.title} will not work as expected.`
: '', : '',
actionText: fixText || 'View', actionText: fixText || 'View',
action: action:
@@ -131,11 +119,10 @@ export class AppShowPage {
} }
private getDepErrors( private getDepErrors(
pkgInstalled: InstalledPackageDataEntry, manifest: Manifest,
depId: string, depId: string,
depErrors: PkgDependencyErrors, depErrors: PkgDependencyErrors,
) { ) {
const pkgManifest = pkgInstalled.manifest
const depError = depErrors[depId] const depError = depErrors[depId]
let errorText: string | null = null let errorText: string | null = null
@@ -146,15 +133,15 @@ export class AppShowPage {
if (depError.type === DependencyErrorType.NotInstalled) { if (depError.type === DependencyErrorType.NotInstalled) {
errorText = 'Not installed' errorText = 'Not installed'
fixText = 'Install' fixText = 'Install'
fixAction = () => this.fixDep(pkgManifest, 'install', depId) fixAction = () => this.fixDep(manifest, 'install', depId)
} else if (depError.type === DependencyErrorType.IncorrectVersion) { } else if (depError.type === DependencyErrorType.IncorrectVersion) {
errorText = 'Incorrect version' errorText = 'Incorrect version'
fixText = 'Update' fixText = 'Update'
fixAction = () => this.fixDep(pkgManifest, 'update', depId) fixAction = () => this.fixDep(manifest, 'update', depId)
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) { } else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config not satisfied' errorText = 'Config not satisfied'
fixText = 'Auto config' fixText = 'Auto config'
fixAction = () => this.fixDep(pkgManifest, 'configure', depId) fixAction = () => this.fixDep(manifest, 'configure', depId)
} else if (depError.type === DependencyErrorType.NotRunning) { } else if (depError.type === DependencyErrorType.NotRunning) {
errorText = 'Not running' errorText = 'Not running'
fixText = 'Start' fixText = 'Start'

View File

@@ -1,5 +1,5 @@
<ion-item-divider>Additional Info</ion-item-divider> <ion-item-divider>Additional Info</ion-item-divider>
<ion-grid *ngIf="pkg.manifest as manifest"> <ion-grid>
<ion-row> <ion-row>
<ion-col responsiveCol sizeXs="12" sizeMd="6"> <ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group> <ion-item-group>

View File

@@ -3,7 +3,7 @@ import { ModalController, ToastController } from '@ionic/angular'
import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared'
import { from } from 'rxjs' import { from } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { Manifest } from 'src/app/services/patch-db/data-model'
@Component({ @Component({
selector: 'app-show-additional', selector: 'app-show-additional',
@@ -12,7 +12,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
}) })
export class AppShowAdditionalComponent { export class AppShowAdditionalComponent {
@Input() @Input()
pkg!: PackageDataEntry manifest!: Manifest
constructor( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
@@ -35,10 +35,16 @@ export class AppShowAdditionalComponent {
} }
async presentModalLicense() { async presentModalLicense() {
const { id, version } = this.manifest
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
componentProps: { componentProps: {
title: 'License', title: 'License',
content: from(this.api.getStatic(this.pkg['static-files']['license'])), content: from(
this.api.getStatic(
`/public/package-data/${id}/${version}/LICENSE.md`,
),
),
}, },
component: MarkdownComponent, component: MarkdownComponent,
}) })

View File

@@ -4,15 +4,12 @@
<ion-back-button defaultHref="services"></ion-back-button> <ion-back-button defaultHref="services"></ion-back-button>
</ion-buttons> </ion-buttons>
<div class="header"> <div class="header">
<img class="logo" [src]="pkg['static-files'].icon" alt="" /> <img class="logo" [src]="pkg.icon" alt="" />
<ion-label> <ion-label *ngIf="pkg | toManifest as manifest">
<h1 <h1 class="montserrat" [class.less-large]="manifest.title.length > 20">
class="montserrat" {{ manifest.title }}
[class.less-large]="pkg.manifest.title.length > 20"
>
{{ pkg.manifest.title }}
</h1> </h1>
<h2>{{ pkg.manifest.version | displayEmver }}</h2> <h2>{{ manifest.version | displayEmver }}</h2>
</ion-label> </ion-label>
</div> </div>
</ion-toolbar> </ion-toolbar>

View File

@@ -1,5 +1,5 @@
<ng-container <ng-container
*ngIf="pkg | toHealthChecks | async | keyvalue: asIsOrder as checks" *ngIf="manifest | toHealthChecks | async | keyvalue: asIsOrder as checks"
> >
<ng-container *ngIf="checks.length"> <ng-container *ngIf="checks.length">
<ion-item-divider>Health Checks</ion-item-divider> <ion-item-divider>Health Checks</ion-item-divider>
@@ -34,7 +34,7 @@
></ion-icon> ></ion-icon>
<ion-label> <ion-label>
<h2 class="bold"> <h2 class="bold">
{{ pkg.manifest['health-checks'][health.key].name }} {{ manifest['health-checks'][health.key].name }}
</h2> </h2>
<ion-text [color]="result | healthColor"> <ion-text [color]="result | healthColor">
<p> <p>
@@ -49,13 +49,11 @@
<span <span
*ngIf=" *ngIf="
result === HealthResult.Success && result === HealthResult.Success &&
pkg.manifest['health-checks'][health.key]['success-message'] manifest['health-checks'][health.key]['success-message']
" "
> >
: :
{{ {{ manifest['health-checks'][health.key]['success-message'] }}
pkg.manifest['health-checks'][health.key]['success-message']
}}
</span> </span>
</p> </p>
</ion-text> </ion-text>
@@ -70,7 +68,7 @@
></ion-spinner> ></ion-spinner>
<ion-label> <ion-label>
<h2 class="bold"> <h2 class="bold">
{{ pkg.manifest['health-checks'][health.key].name }} {{ manifest['health-checks'][health.key].name }}
</h2> </h2>
<p class="primary">Awaiting result...</p> <p class="primary">Awaiting result...</p>
</ion-label> </ion-label>

View File

@@ -1,9 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { import { HealthResult, Manifest } from 'src/app/services/patch-db/data-model'
HealthResult,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
@Component({ @Component({
selector: 'app-show-health-checks', selector: 'app-show-health-checks',
@@ -13,7 +10,7 @@ import {
}) })
export class AppShowHealthChecksComponent { export class AppShowHealthChecksComponent {
@Input() @Input()
pkg!: PackageDataEntry manifest!: Manifest
HealthResult = HealthResult HealthResult = HealthResult

View File

@@ -1,20 +1,18 @@
<p>Downloading: {{ progressData.downloadProgress }}%</p> <ng-container *ngFor="let phase of phases">
<ion-progress-bar <p>
[color]="getColor('download-complete')" {{ phase.name }}
[value]="progressData.downloadProgress / 100" <span *ngIf="phase.progress | installingProgress as progress">
[buffer]="!progressData.downloadProgress ? 0 : 1" : {{ progress * 100 }}%
></ion-progress-bar> </span>
</p>
<p>Validating: {{ progressData.validateProgress }}%</p> <ion-progress-bar
<ion-progress-bar [type]="
[color]="getColor('validation-complete')" phase.progress === true ||
[value]="progressData.validateProgress / 100" (phase.progress !== false && phase.progress.total)
[buffer]="validationBuffer" ? 'determinate'
></ion-progress-bar> : 'indeterminate'
"
<p>Unpacking: {{ progressData.unpackProgress }}%</p> [color]="phase.progress === true ? 'success' : 'secondary'"
<ion-progress-bar [value]="phase.progress | installingProgress"
[color]="getColor('unpack-complete')" ></ion-progress-bar>
[value]="progressData.unpackProgress / 100" </ng-container>
[buffer]="unpackingBuffer"
></ion-progress-bar>

View File

@@ -1,9 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { import { FullProgress } from 'src/app/services/patch-db/data-model'
InstallProgress,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { ProgressData } from 'src/app/types/progress-data'
@Component({ @Component({
selector: 'app-show-progress', selector: 'app-show-progress',
@@ -13,26 +9,5 @@ import { ProgressData } from 'src/app/types/progress-data'
}) })
export class AppShowProgressComponent { export class AppShowProgressComponent {
@Input() @Input()
pkg!: PackageDataEntry phases!: FullProgress['phases']
@Input()
progressData!: ProgressData
get unpackingBuffer(): number {
return this.progressData.validateProgress === 100 &&
!this.progressData.unpackProgress
? 0
: 1
}
get validationBuffer(): number {
return this.progressData.downloadProgress === 100 &&
!this.progressData.validateProgress
? 0
: 1
}
getColor(action: keyof InstallProgress): string {
return this.pkg['install-progress']?.[action] ? 'success' : 'secondary'
}
} }

View File

@@ -4,14 +4,14 @@
<status <status
size="x-large" size="x-large"
weight="600" weight="600"
[installProgress]="pkg['install-progress']" [installingInfo]="$any(pkg['state-info'])['installing-info']"
[rendering]="PR[status.primary]" [rendering]="PR[status.primary]"
[sigtermTimeout]="sigtermTimeout" [sigtermTimeout]="sigtermTimeout"
></status> ></status>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ng-container *ngIf="isInstalled && (connected$ | async)"> <ng-container *ngIf="isInstalled(pkg) && (connected$ | async)">
<ion-grid> <ion-grid>
<ion-row style="padding-left: 12px"> <ion-row style="padding-left: 12px">
<ion-col> <ion-col>
@@ -59,7 +59,9 @@
*ngIf="pkgStatus && interfaces && (interfaces | hasUi)" *ngIf="pkgStatus && interfaces && (interfaces | hasUi)"
class="action-button" class="action-button"
color="primary" color="primary"
[disabled]="!(pkg.state | isLaunchable: pkgStatus.main.status)" [disabled]="
!(pkg['state-info'].state | isLaunchable: pkgStatus.main.status)
"
(click)="launchUi(interfaces)" (click)="launchUi(interfaces)"
> >
<ion-icon slot="start" name="open-outline"></ion-icon> <ion-icon slot="start" name="open-outline"></ion-icon>

View File

@@ -6,7 +6,7 @@ import {
PrimaryStatus, PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service' } from 'src/app/services/pkg-status-rendering.service'
import { import {
InstalledPackageDataEntry, Manifest,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
@@ -18,6 +18,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { isInstalled, getManifest } from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'app-show-status', selector: 'app-show-status',
@@ -34,6 +35,8 @@ export class AppShowStatusComponent {
PR = PrimaryRendering PR = PrimaryRendering
isInstalled = isInstalled
readonly connected$ = this.connectionService.connected$ readonly connected$ = this.connectionService.connected$
constructor( constructor(
@@ -46,18 +49,16 @@ export class AppShowStatusComponent {
private readonly connectionService: ConnectionService, private readonly connectionService: ConnectionService,
) {} ) {}
get interfaces(): get interfaces(): PackageDataEntry['service-interfaces'] {
| InstalledPackageDataEntry['service-interfaces'] return this.pkg['service-interfaces']
| undefined {
return this.pkg.installed?.['service-interfaces']
} }
get pkgStatus(): Status | null { get pkgStatus(): Status {
return this.pkg.installed?.status || null return this.pkg.status
} }
get isInstalled(): boolean { get manifest(): Manifest {
return this.pkg.state === PackageState.Installed return getManifest(this.pkg)
} }
get isRunning(): boolean { get isRunning(): boolean {
@@ -82,25 +83,25 @@ export class AppShowStatusComponent {
: null : null
} }
launchUi(interfaces: InstalledPackageDataEntry['service-interfaces']): void { launchUi(interfaces: PackageDataEntry['service-interfaces']): void {
this.launcherService.launch(interfaces) this.launcherService.launch(interfaces)
} }
async presentModalConfig(): Promise<void> { async presentModalConfig(): Promise<void> {
return this.modalService.presentModalConfig({ return this.modalService.presentModalConfig({
pkgId: this.id, pkgId: this.manifest.id,
}) })
} }
async tryStart(): Promise<void> { async tryStart(): Promise<void> {
if (this.status.dependency === 'warning') { if (this.status.dependency === 'warning') {
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.` const depErrMsg = `${this.manifest.title} has unmet dependencies. It will not work as expected.`
const proceed = await this.presentAlertStart(depErrMsg) const proceed = await this.presentAlertStart(depErrMsg)
if (!proceed) return if (!proceed) return
} }
const alertMsg = this.pkg.manifest.alerts.start const alertMsg = this.manifest.alerts.start
if (alertMsg) { if (alertMsg) {
const proceed = await this.presentAlertStart(alertMsg) const proceed = await this.presentAlertStart(alertMsg)
@@ -112,7 +113,7 @@ export class AppShowStatusComponent {
} }
async tryStop(): Promise<void> { async tryStop(): Promise<void> {
const { title, alerts } = this.pkg.manifest const { title, alerts } = this.manifest
let message = alerts.stop || '' let message = alerts.stop || ''
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(this.pkg)) {
@@ -150,7 +151,7 @@ export class AppShowStatusComponent {
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(this.pkg)) {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Warning', header: 'Warning',
message: `Services that depend on ${this.pkg.manifest.title} may temporarily experiences issues`, message: `Services that depend on ${this.manifest.title} may temporarily experiences issues`,
buttons: [ buttons: [
{ {
text: 'Cancel', text: 'Cancel',
@@ -173,10 +174,6 @@ export class AppShowStatusComponent {
} }
} }
private get id(): string {
return this.pkg.manifest.id
}
private async start(): Promise<void> { private async start(): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: `Starting...`, message: `Starting...`,
@@ -184,7 +181,7 @@ export class AppShowStatusComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.startPackage({ id: this.id }) await this.embassyApi.startPackage({ id: this.manifest.id })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -199,7 +196,7 @@ export class AppShowStatusComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.stopPackage({ id: this.id }) await this.embassyApi.stopPackage({ id: this.manifest.id })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -214,7 +211,7 @@ export class AppShowStatusComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.restartPackage({ id: this.id }) await this.embassyApi.restartPackage({ id: this.manifest.id })
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {

View File

@@ -1,13 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ProgressData } from 'src/app/types/progress-data'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
@Pipe({
name: 'progressData',
})
export class ProgressDataPipe implements PipeTransform {
transform(pkg: PackageDataEntry): ProgressData | null {
return packageLoadingProgress(pkg['install-progress'])
}
}

View File

@@ -4,12 +4,15 @@ import { ModalController, NavController } from '@ionic/angular'
import { MarkdownComponent } from '@start9labs/shared' import { MarkdownComponent } from '@start9labs/shared'
import { import {
DataModel, DataModel,
InstalledState,
Manifest,
PackageDataEntry, PackageDataEntry,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { from, map, Observable } from 'rxjs' import { from, map, Observable } from 'rxjs'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { getManifest } from 'src/app/util/get-package-data'
export interface Button { export interface Button {
title: string title: string
@@ -33,26 +36,26 @@ export class ToButtonsPipe implements PipeTransform {
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
transform(pkg: PackageDataEntry): Button[] { transform(pkg: PackageDataEntry<InstalledState>): Button[] {
const pkgTitle = pkg.manifest.title const manifest = pkg['state-info'].manifest
return [ return [
// instructions // instructions
{ {
action: () => this.presentModalInstructions(pkg), action: () => this.presentModalInstructions(manifest),
title: 'Instructions', title: 'Instructions',
description: `Understand how to use ${pkgTitle}`, description: `Understand how to use ${manifest.title}`,
icon: 'list-outline', icon: 'list-outline',
highlighted$: this.patch highlighted$: this.patch
.watch$('ui', 'ack-instructions', pkg.manifest.id) .watch$('ui', 'ack-instructions', manifest.id)
.pipe(map(seen => !seen)), .pipe(map(seen => !seen)),
}, },
// config // config
{ {
action: async () => action: async () =>
this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }), this.modalService.presentModalConfig({ pkgId: manifest.id }),
title: 'Config', title: 'Config',
description: `Customize ${pkgTitle}`, description: `Customize ${manifest.title}`,
icon: 'options-outline', icon: 'options-outline',
}, },
// properties // properties
@@ -71,7 +74,7 @@ export class ToButtonsPipe implements PipeTransform {
action: () => action: () =>
this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }), this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
title: 'Actions', title: 'Actions',
description: `Uninstall and other commands specific to ${pkgTitle}`, description: `Uninstall and other commands specific to ${manifest.title}`,
icon: 'flash-outline', icon: 'flash-outline',
}, },
// interfaces // interfaces
@@ -97,16 +100,18 @@ export class ToButtonsPipe implements PipeTransform {
] ]
} }
private async presentModalInstructions(pkg: PackageDataEntry) { private async presentModalInstructions(manifest: Manifest) {
this.apiService this.apiService
.setDbValue<boolean>(['ack-instructions', pkg.manifest.id], true) .setDbValue<boolean>(['ack-instructions', manifest.id], true)
.catch(e => console.error('Failed to mark instructions as seen', e)) .catch(e => console.error('Failed to mark instructions as seen', e))
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
componentProps: { componentProps: {
title: 'Instructions', title: 'Instructions',
content: from( content: from(
this.apiService.getStatic(pkg['static-files']['instructions']), this.apiService.getStatic(
`/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`,
),
), ),
}, },
component: MarkdownComponent, component: MarkdownComponent,
@@ -115,17 +120,22 @@ export class ToButtonsPipe implements PipeTransform {
await modal.present() await modal.present()
} }
private viewInMarketplaceButton(pkg: PackageDataEntry): Button { private viewInMarketplaceButton(
const url = pkg.installed?.['marketplace-url'] pkg: PackageDataEntry<InstalledState>,
): Button {
const url = pkg['marketplace-url']
const queryParams = url ? { url } : {} const queryParams = url ? { url } : {}
let button: Button = { let button: Button = {
title: 'Marketplace Listing', title: 'Marketplace Listing',
icon: 'storefront-outline', icon: 'storefront-outline',
action: () => action: () =>
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], { this.navCtrl.navigateForward(
queryParams, [`marketplace/${pkg['state-info'].manifest.id}`],
}), {
queryParams,
},
),
disabled: false, disabled: false,
description: 'View service in the marketplace', description: 'View service in the marketplace',
} }

View File

@@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'
import { import {
DataModel, DataModel,
HealthCheckResult, HealthCheckResult,
PackageDataEntry, Manifest,
PackageMainStatus, PackageMainStatus,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { isEmptyObject } from '@start9labs/shared' import { isEmptyObject } from '@start9labs/shared'
@@ -17,15 +17,15 @@ export class ToHealthChecksPipe implements PipeTransform {
constructor(private readonly patch: PatchDB<DataModel>) {} constructor(private readonly patch: PatchDB<DataModel>) {}
transform( transform(
pkg: PackageDataEntry, manifest: Manifest,
): Observable<Record<string, HealthCheckResult | null>> | null { ): Observable<Record<string, HealthCheckResult | null>> | null {
const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce( const healthChecks = Object.keys(manifest['health-checks']).reduce(
(obj, key) => ({ ...obj, [key]: null }), (obj, key) => ({ ...obj, [key]: null }),
{}, {},
) )
const healthChecks$ = this.patch const healthChecks$ = this.patch
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main') .watch$('package-data', manifest.id, 'status', 'main')
.pipe( .pipe(
map(main => { map(main => {
// Question: is this ok or do we have to use Object.keys // Question: is this ok or do we have to use Object.keys

View File

@@ -8,7 +8,7 @@
View Installed View Installed
</ion-button> </ion-button>
<ng-container *ngIf="localPkg; else install"> <ng-container *ngIf="localPkg; else install">
<ng-container *ngIf="localPkg.state === PackageState.Installed"> <ng-container *ngIf="localPkg['state-info'].state === 'installed'">
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1" *ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
expand="block" expand="block"

View File

@@ -24,7 +24,7 @@ import { ClientStorageService } from 'src/app/services/client-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { getAllPackages } from 'src/app/util/get-package-data' import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
import { firstValueFrom } from 'rxjs' import { firstValueFrom } from 'rxjs'
import { dryUpdate } from 'src/app/util/dry-update' import { dryUpdate } from 'src/app/util/dry-update'
@@ -46,8 +46,6 @@ export class MarketplaceShowControlsComponent {
readonly showDevTools$ = this.ClientStorageService.showDevTools$ readonly showDevTools$ = this.ClientStorageService.showDevTools$
readonly PackageState = PackageState
constructor( constructor(
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
private readonly ClientStorageService: ClientStorageService, private readonly ClientStorageService: ClientStorageService,
@@ -60,7 +58,7 @@ export class MarketplaceShowControlsComponent {
) {} ) {}
get localVersion(): string { get localVersion(): string {
return this.localPkg?.manifest.version || '' return this.localPkg ? getManifest(this.localPkg).version : ''
} }
async tryInstall() { async tryInstall() {
@@ -72,7 +70,7 @@ export class MarketplaceShowControlsComponent {
if (!this.localPkg) { if (!this.localPkg) {
this.alertInstall(url) this.alertInstall(url)
} else { } else {
const originalUrl = this.localPkg.installed?.['marketplace-url'] const originalUrl = this.localPkg['marketplace-url']
if (!sameUrl(url, originalUrl)) { if (!sameUrl(url, originalUrl)) {
const proceed = await this.presentAlertDifferentMarketplace( const proceed = await this.presentAlertDifferentMarketplace(

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="localPkg" [ngSwitch]="localPkg.state"> <ng-container *ngIf="localPkg">
<div *ngSwitchCase="PackageState.Installed"> <div *ngIf="isInstalled(localPkg)">
<ion-text <ion-text
*ngIf="(version | compareEmver: localVersion) !== 1" *ngIf="(version | compareEmver: localVersion) !== 1"
color="primary" color="primary"
@@ -13,15 +13,24 @@
Update Available Update Available
</ion-text> </ion-text>
</div> </div>
<div *ngSwitchCase="PackageState.Removing">
<div *ngIf="isRemoving(localPkg)">
<ion-text color="danger"> <ion-text color="danger">
Removing Removing
<span class="loading-dots"></span> <span class="loading-dots"></span>
</ion-text> </ion-text>
</div> </div>
<div *ngSwitchDefault>
<div
*ngIf="
isInstalling(localPkg) || isUpdating(localPkg) || isRestoring(localPkg)
"
>
<ion-text <ion-text
*ngIf="localPkg['install-progress'] | installProgressDisplay as progress" *ngIf="
localPkg['state-info']['installing-info']!.progress.overall
| installingProgressString as progress
"
color="primary" color="primary"
> >
Installing Installing

View File

@@ -1,8 +1,13 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { import {
PackageDataEntry, isInstalled,
PackageState, isInstalling,
} from 'src/app/services/patch-db/data-model' isUpdating,
isRemoving,
isRestoring,
getManifest,
} from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'marketplace-status', selector: 'marketplace-status',
@@ -14,9 +19,13 @@ export class MarketplaceStatusComponent {
@Input() localPkg?: PackageDataEntry @Input() localPkg?: PackageDataEntry
PackageState = PackageState isInstalled = isInstalled
isInstalling = isInstalling
isUpdating = isUpdating
isRemoving = isRemoving
isRestoring = isRestoring
get localVersion(): string { get localVersion(): string {
return this.localPkg?.manifest.version || '' return this.localPkg ? getManifest(this.localPkg).version : ''
} }
} }

View File

@@ -2,8 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { EmverPipesModule } from '@start9labs/shared' import { EmverPipesModule } from '@start9labs/shared'
import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
import { InstallProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
import { MarketplaceStatusComponent } from './marketplace-status.component' import { MarketplaceStatusComponent } from './marketplace-status.component'
@NgModule({ @NgModule({
@@ -11,7 +10,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component'
CommonModule, CommonModule,
IonicModule, IonicModule,
EmverPipesModule, EmverPipesModule,
InstallProgressPipeModule, InstallingProgressPipeModule,
], ],
declarations: [MarketplaceStatusComponent], declarations: [MarketplaceStatusComponent],
exports: [MarketplaceStatusComponent], exports: [MarketplaceStatusComponent],

View File

@@ -6,6 +6,7 @@ import { NotificationsPage } from './notifications.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -22,6 +23,7 @@ const routes: Routes = [
BadgeMenuComponentModule, BadgeMenuComponentModule,
SharedPipesModule, SharedPipesModule,
BackupReportPageModule, BackupReportPageModule,
UiPipeModule,
], ],
declarations: [NotificationsPage], declarations: [NotificationsPage],
}) })

View File

@@ -87,8 +87,8 @@
<h2> <h2>
<b> <b>
<span *ngIf="not['package-id'] as pkgId"> <span *ngIf="not['package-id'] as pkgId">
<!-- @TODO remove $any when Angular gets smart enough --> {{ packageData[pkgId] ? (packageData[pkgId] |
{{ $any(packageData[pkgId])?.manifest.title || pkgId }} - toManifest).title : pkgId }} -
</span> </span>
<ion-text [color]="getColor(not)">{{ not.title }}</ion-text> <ion-text [color]="getColor(not)">{{ not.title }}</ion-text>
</b> </b>

View File

@@ -15,9 +15,9 @@
<ng-container *ngFor="let pkg of pkgs | keyvalue"> <ng-container *ngFor="let pkg of pkgs | keyvalue">
<ion-item *ngIf="backupProgress[pkg.key] as pkgProgress"> <ion-item *ngIf="backupProgress[pkg.key] as pkgProgress">
<ion-avatar slot="start"> <ion-avatar slot="start">
<img [src]="pkg.value['static-files'].icon" /> <img [src]="pkg.value.icon" />
</ion-avatar> </ion-avatar>
<ion-label>{{ pkg.value.manifest.title }}</ion-label> <ion-label>{{ (pkg.value | toManifest).title }}</ion-label>
<!-- complete --> <!-- complete -->
<ion-note <ion-note
*ngIf="pkgProgress.complete; else incomplete" *ngIf="pkgProgress.complete; else incomplete"
@@ -35,10 +35,7 @@
> >
<!-- active --> <!-- active -->
<ion-note <ion-note
*ngIf=" *ngIf="pkgStatus === 'backing-up'; else queued"
pkgStatus === PackageMainStatus.BackingUp;
else queued
"
class="inline" class="inline"
slot="end" slot="end"
> >

View File

@@ -25,8 +25,6 @@ export class BackingUpComponent {
'backup-progress', 'backup-progress',
) )
PackageMainStatus = PackageMainStatus
constructor(private readonly patch: PatchDB<DataModel>) {} constructor(private readonly patch: PatchDB<DataModel>) {}
} }
@@ -35,14 +33,7 @@ export class BackingUpComponent {
}) })
export class PkgMainStatusPipe implements PipeTransform { export class PkgMainStatusPipe implements PipeTransform {
transform(pkgId: string): Observable<PackageMainStatus> { transform(pkgId: string): Observable<PackageMainStatus> {
return this.patch.watch$( return this.patch.watch$('package-data', pkgId, 'status', 'main', 'status')
'package-data',
pkgId,
'installed',
'status',
'main',
'status',
)
} }
constructor(private readonly patch: PatchDB<DataModel>) {} constructor(private readonly patch: PatchDB<DataModel>) {}

View File

@@ -8,6 +8,7 @@ import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/ba
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module' import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module'
import { PkgMainStatusPipe } from './backing-up/backing-up.component' import { PkgMainStatusPipe } from './backing-up/backing-up.component'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -24,6 +25,7 @@ const routes: Routes = [
SharedPipesModule, SharedPipesModule,
BackupDrivesComponentModule, BackupDrivesComponentModule,
BackupSelectPageModule, BackupSelectPageModule,
UiPipeModule,
], ],
declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe], declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe],
}) })

View File

@@ -12,7 +12,7 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
import { RoundProgressModule } from 'angular-svg-round-progressbar' import { RoundProgressModule } from 'angular-svg-round-progressbar'
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
import { MimeTypePipeModule } from '@start9labs/marketplace' import { MimeTypePipeModule } from '@start9labs/marketplace'
@@ -34,7 +34,7 @@ const routes: Routes = [
SkeletonListComponentModule, SkeletonListComponentModule,
MarkdownPipeModule, MarkdownPipeModule,
RoundProgressModule, RoundProgressModule,
InstallProgressPipeModule, InstallingProgressPipeModule,
StoreIconComponentModule, StoreIconComponentModule,
EmverPipesModule, EmverPipesModule,
MimeTypePipeModule, MimeTypePipeModule,

View File

@@ -39,8 +39,8 @@
<h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1> <h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1>
<h2 class="inline"> <h2 class="inline">
<span> <span>
{{ local.installed?.manifest?.version || '' | {{ local['state-info'].manifest.version | displayEmver
displayEmver }} }}
</span> </span>
&nbsp; &nbsp;
<ion-icon name="arrow-forward"></ion-icon> <ion-icon name="arrow-forward"></ion-icon>
@@ -57,8 +57,8 @@
</ion-label> </ion-label>
<div slot="end" style="margin-left: 4px"> <div slot="end" style="margin-left: 4px">
<round-progress <round-progress
*ngIf="local.state === 'updating' else notUpdating" *ngIf="local['state-info'].state === 'updating' else notUpdating"
[current]="local['install-progress'] | installProgress" [current]="(local['state-info']['installing-info'].progress.overall | installingProgress) || 0"
[max]="100" [max]="100"
[radius]="13" [radius]="13"
[stroke]="3" [stroke]="3"

View File

@@ -2,7 +2,9 @@ import { Component, Inject } from '@angular/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
DataModel, DataModel,
InstalledState,
PackageDataEntry, PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
import { import {
@@ -14,16 +16,20 @@ import {
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { Emver, isEmptyObject } from '@start9labs/shared' import { Emver, isEmptyObject } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { combineLatest, Observable } from 'rxjs' import { combineLatest, map, Observable } from 'rxjs'
import { AlertController, NavController } from '@ionic/angular' import { AlertController, NavController } from '@ionic/angular'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getAllPackages } from 'src/app/util/get-package-data' import {
getAllPackages,
isInstalled,
isUpdating,
} from 'src/app/util/get-package-data'
import { dryUpdate } from 'src/app/util/dry-update' import { dryUpdate } from 'src/app/util/dry-update'
interface UpdatesData { interface UpdatesData {
hosts: StoreIdentity[] hosts: StoreIdentity[]
marketplace: Marketplace marketplace: Marketplace
localPkgs: Record<string, PackageDataEntry> localPkgs: Record<string, PackageDataEntry<InstalledState | UpdatingState>>
errors: string[] errors: string[]
} }
@@ -36,7 +42,14 @@ export class UpdatesPage {
readonly data$: Observable<UpdatesData> = combineLatest({ readonly data$: Observable<UpdatesData> = combineLatest({
hosts: this.marketplaceService.getKnownHosts$(true), hosts: this.marketplaceService.getKnownHosts$(true),
marketplace: this.marketplaceService.getMarketplace$(), marketplace: this.marketplaceService.getMarketplace$(),
localPkgs: this.patch.watch$('package-data'), localPkgs: this.patch.watch$('package-data').pipe(
map(pkgs =>
Object.values(pkgs).reduce((acc, curr) => {
if (isInstalled(curr) || isUpdating(curr)) return { ...acc, curr }
return acc
}, {} as Record<string, PackageDataEntry<InstalledState | UpdatingState>>),
),
),
errors: this.marketplaceService.getRequestErrors$(), errors: this.marketplaceService.getRequestErrors$(),
}) })
@@ -154,14 +167,17 @@ export class FilterUpdatesPipe implements PipeTransform {
transform( transform(
pkgs: MarketplacePkg[], pkgs: MarketplacePkg[],
local: Record<string, PackageDataEntry | undefined>, local: Record<string, PackageDataEntry<InstalledState | UpdatingState>>,
): MarketplacePkg[] { ): MarketplacePkg[] {
return pkgs.filter( return pkgs.filter(({ manifest }) => {
({ manifest }) => const localPkg = local[manifest.id]
return (
localPkg &&
this.emver.compare( this.emver.compare(
manifest.version, manifest.version,
local[manifest.id]?.installed?.manifest.version || '', localPkg['state-info'].manifest.version,
) === 1, ) === 1
) )
})
} }
} }

View File

@@ -9,6 +9,7 @@ import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info' import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info'
import { combineLatest } from 'rxjs' import { combineLatest } from 'rxjs'
import { DepErrorService } from 'src/app/services/dep-error.service' import { DepErrorService } from 'src/app/services/dep-error.service'
import { getManifest } from 'src/app/util/get-package-data'
@Component({ @Component({
selector: 'widget-health', selector: 'widget-health',
@@ -31,7 +32,7 @@ export class HealthComponent {
]).pipe( ]).pipe(
map(([data, depErrors]) => { map(([data, depErrors]) => {
const pkgs = Object.values<PackageDataEntry>(data).map(pkg => const pkgs = Object.values<PackageDataEntry>(data).map(pkg =>
getPackageInfo(pkg, depErrors[pkg.manifest.id]), getPackageInfo(pkg, depErrors[getManifest(pkg).id]),
) )
const result = this.labels.reduce<Record<string, number>>( const result = this.labels.reduce<Record<string, number>>(
(acc, label) => ({ (acc, label) => ({

View File

@@ -1,11 +1,11 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { import {
InstallProgressDisplayPipe, InstallingProgressDisplayPipe,
InstallProgressPipe, InstallingProgressPipe,
} from './install-progress.pipe' } from './install-progress.pipe'
@NgModule({ @NgModule({
declarations: [InstallProgressPipe, InstallProgressDisplayPipe], declarations: [InstallingProgressPipe, InstallingProgressDisplayPipe],
exports: [InstallProgressPipe, InstallProgressDisplayPipe], exports: [InstallingProgressPipe, InstallingProgressDisplayPipe],
}) })
export class InstallProgressPipeModule {} export class InstallingProgressPipeModule {}

View File

@@ -1,24 +1,32 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { InstallProgress } from 'src/app/services/patch-db/data-model' import { Progress } from 'src/app/services/patch-db/data-model'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
@Pipe({ @Pipe({
name: 'installProgress', name: 'installingProgressString',
}) })
export class InstallProgressPipe implements PipeTransform { export class InstallingProgressDisplayPipe implements PipeTransform {
transform(installProgress?: InstallProgress): number { transform(progress: Progress): string {
return packageLoadingProgress(installProgress)?.totalProgress || 0 if (progress === true) return 'finalizing'
if (progress === false || !progress.total) return 'unknown %'
const percentage = Math.round((100 * progress.done) / progress.total)
return percentage < 99 ? String(percentage) + '%' : 'finalizing'
} }
} }
@Pipe({ @Pipe({
name: 'installProgressDisplay', name: 'installingProgress',
}) })
export class InstallProgressDisplayPipe implements PipeTransform { export class InstallingProgressPipe implements PipeTransform {
transform(installProgress?: InstallProgress): string { transform(progress: Progress): number | null {
const totalProgress = if (progress === true) return 1
packageLoadingProgress(installProgress)?.totalProgress || 0 if (progress === false || !progress.total) return null
return Number((progress.done / progress.total).toFixed(2))
return totalProgress < 99 ? totalProgress + '%' : 'finalizing'
} }
} }
function getProgress(progress: Progress): number | null {
if (progress === true) return 1
if (progress === false || !progress.total) return null
return Number((progress.done / progress.total).toFixed(2))
}

View File

@@ -1,8 +1,8 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { UiPipe } from './ui.pipe' import { ToManifestPipe, UiPipe } from './ui.pipe'
@NgModule({ @NgModule({
declarations: [UiPipe], declarations: [UiPipe, ToManifestPipe],
exports: [UiPipe], exports: [UiPipe, ToManifestPipe],
}) })
export class UiPipeModule {} export class UiPipeModule {}

View File

@@ -1,14 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { InstalledPackageDataEntry } from '../../services/patch-db/data-model' import { Manifest, PackageDataEntry } from '../../services/patch-db/data-model'
import { hasUi } from '../../services/config.service' import { hasUi } from '../../services/config.service'
import { getManifest } from 'src/app/util/get-package-data'
@Pipe({ @Pipe({
name: 'hasUi', name: 'hasUi',
}) })
export class UiPipe implements PipeTransform { export class UiPipe implements PipeTransform {
transform( transform(interfaces: PackageDataEntry['service-interfaces']): boolean {
interfaces: InstalledPackageDataEntry['service-interfaces'],
): boolean {
return interfaces ? hasUi(interfaces) : false return interfaces ? hasUi(interfaces) : false
} }
} }
@Pipe({
name: 'toManifest',
})
export class ToManifestPipe implements PipeTransform {
transform(pkg: PackageDataEntry): Manifest {
return getManifest(pkg)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,11 +10,14 @@ import {
} from 'patch-db-client' } from 'patch-db-client'
import { import {
DataModel, DataModel,
InstallProgress, FullProgress,
InstallingState,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
ServerStatus, ServerStatus,
StateInfo,
UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { CifsBackupTarget, RR } from './api.types' import { CifsBackupTarget, RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { parsePropertiesPermissive } from 'src/app/util/properties.util'
@@ -39,14 +42,34 @@ import { AuthService } from '../auth.service'
import { ConnectionService } from '../connection.service' import { ConnectionService } from '../connection.service'
import { StoreInfo } from '@start9labs/marketplace' import { StoreInfo } from '@start9labs/marketplace'
const PROGRESS: InstallProgress = { const PROGRESS: FullProgress = {
size: 120, overall: {
downloaded: 0, done: 0,
'download-complete': false, total: 120,
validated: 0, },
'validation-complete': false, phases: [
unpacked: 0, {
'unpack-complete': false, name: 'Downloading',
progress: {
done: 0,
total: 40,
},
},
{
name: 'Validating',
progress: {
done: 0,
total: 40,
},
},
{
name: 'Installing',
progress: {
done: 0,
total: 40,
},
},
],
} }
@Injectable() @Injectable()
@@ -656,15 +679,30 @@ export class MockApiService extends ApiService {
this.updateProgress(params.id) this.updateProgress(params.id)
}, 1000) }, 1000)
const patch: Operation<PackageDataEntry>[] = [ const manifest = Mock.LocalPkgs[params.id]['state-info'].manifest
const patch: Operation<
PackageDataEntry<InstallingState | UpdatingState>
>[] = [
{ {
op: PatchOp.ADD, op: PatchOp.ADD,
path: `/package-data/${params.id}`, path: `/package-data/${params.id}`,
value: { value: {
...Mock.LocalPkgs[params.id], ...Mock.LocalPkgs[params.id],
// state: PackageState.Installing, 'state-info': {
state: PackageState.Updating, // if installing
'install-progress': { ...PROGRESS }, state: PackageState.Installing,
// if updating
// state: PackageState.Updating,
// manifest,
// both
'installing-info': {
'new-manifest': manifest,
progress: PROGRESS,
},
},
}, },
}, },
] ]
@@ -720,9 +758,13 @@ export class MockApiService extends ApiService {
path: `/package-data/${id}`, path: `/package-data/${id}`,
value: { value: {
...Mock.LocalPkgs[id], ...Mock.LocalPkgs[id],
state: PackageState.Restoring, 'state-info': {
'install-progress': { ...PROGRESS }, state: PackageState.Restoring,
installed: undefined, 'installing-info': {
'new-manifest': Mock.LocalPkgs[id]['state-info'].manifest!,
progress: PROGRESS,
},
},
}, },
} }
}) })
@@ -948,7 +990,7 @@ export class MockApiService extends ApiService {
const patch = [ const patch = [
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: `/package-data/${params.id}/state`, path: `/package-data/${params.id}/state-info/state`,
value: PackageState.Removing, value: PackageState.Removing,
}, },
] ]
@@ -977,55 +1019,100 @@ export class MockApiService extends ApiService {
} }
private async updateProgress(id: string): Promise<void> { private async updateProgress(id: string): Promise<void> {
const progress = { ...PROGRESS } const progress = JSON.parse(JSON.stringify(PROGRESS))
const phases = [
{ progress: 'downloaded', completion: 'download-complete' },
{ progress: 'validated', completion: 'validation-complete' },
{ progress: 'unpacked', completion: 'unpack-complete' },
] as const
for (let phase of phases) { for (let [i, phase] of progress.phases.entries()) {
let i = progress[phase.progress] if (typeof phase.progress !== 'object' || !phase.progress.total) {
const size = progress?.size || 0 await pauseFor(2000)
while (i < size) {
await pauseFor(250)
i = Math.min(i + 5, size)
progress[phase.progress] = i
if (i === progress.size) { const patches: Operation<any>[] = [
progress[phase.completion] = true
}
const patch = [
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: `/package-data/${id}/install-progress`, path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress`,
value: { ...progress }, value: true,
}, },
] ]
this.mockRevision(patch)
// overall
if (typeof progress.overall === 'object' && progress.overall.total) {
const step = progress.overall.total / progress.phases.length
progress.overall.done += step
patches.push({
op: PatchOp.REPLACE,
path: `/package-data/${id}/state-info/installing-info/progress/overall/done`,
value: progress.overall.done,
})
}
this.mockRevision(patches)
} else {
const step = phase.progress.total / 4
while (phase.progress.done < phase.progress.total) {
await pauseFor(500)
phase.progress.done += step
const patches: Operation<any>[] = [
{
op: PatchOp.REPLACE,
path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress/done`,
value: phase.progress.done,
},
]
// overall
if (typeof progress.overall === 'object' && progress.overall.total) {
const step = progress.overall.total / progress.phases.length / 4
progress.overall.done += step
patches.push({
op: PatchOp.REPLACE,
path: `/package-data/${id}/state-info/installing-info/progress/overall/done`,
value: progress.overall.done,
})
}
this.mockRevision(patches)
if (phase.progress.done === phase.progress.total) {
await pauseFor(250)
this.mockRevision([
{
op: PatchOp.REPLACE,
path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress`,
value: true,
},
])
}
}
} }
} }
setTimeout(() => { await pauseFor(1000)
const patch2: Operation<any>[] = [ this.mockRevision([
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: `/package-data/${id}/state`, path: `/package-data/${id}/state-info/installing-info/progress/overall`,
value: PackageState.Installed, value: true,
},
])
await pauseFor(1000)
const patch2: Operation<StateInfo>[] = [
{
op: PatchOp.REPLACE,
path: `/package-data/${id}/state-info`,
value: {
state: PackageState.Installed,
manifest: Mock.LocalPkgs[id]['state-info'].manifest,
}, },
{ },
op: PatchOp.ADD, ]
path: `/package-data/${id}/installed`, this.mockRevision(patch2)
value: { ...Mock.LocalPkgs[id].installed },
},
{
op: PatchOp.REMOVE,
path: `/package-data/${id}/install-progress`,
},
]
this.mockRevision(patch2)
}, 1000)
} }
private async updateOSProgress() { private async updateOSProgress() {

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'
import { WorkspaceConfig } from '@start9labs/shared' import { WorkspaceConfig } from '@start9labs/shared'
import { types } from '@start9labs/start-sdk' import { types } from '@start9labs/start-sdk'
import { import {
InstalledPackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
@@ -64,7 +64,7 @@ export class ConfigService {
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */ /** ${scheme}://${username}@${host}:${externalPort}${suffix} */
launchableAddress( launchableAddress(
interfaces: InstalledPackageDataEntry['service-interfaces'], interfaces: PackageDataEntry['service-interfaces'],
): string { ): string {
const ui = Object.values(interfaces).find(i => i.type === 'ui') const ui = Object.values(interfaces).find(i => i.type === 'ui')
@@ -128,7 +128,7 @@ export class ConfigService {
} }
export function hasUi( export function hasUi(
interfaces: InstalledPackageDataEntry['service-interfaces'], interfaces: PackageDataEntry['service-interfaces'],
): boolean { ): boolean {
return Object.values(interfaces).some(iface => iface.type === 'ui') return Object.values(interfaces).some(iface => iface.type === 'ui')
} }

View File

@@ -4,12 +4,14 @@ import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
DataModel, DataModel,
HealthCheckResult,
HealthResult, HealthResult,
InstalledPackageDataEntry, InstalledState,
PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState,
} from './patch-db/data-model' } from './patch-db/data-model'
import * as deepEqual from 'fast-deep-equal' import * as deepEqual from 'fast-deep-equal'
import { isInstalled } from '../util/get-package-data'
export type AllDependencyErrors = Record<string, PkgDependencyErrors> export type AllDependencyErrors = Record<string, PkgDependencyErrors>
export type PkgDependencyErrors = Record<string, DependencyError | null> export type PkgDependencyErrors = Record<string, DependencyError | null>
@@ -55,14 +57,14 @@ export class DepErrorService {
pkgId: string, pkgId: string,
outerErrors: AllDependencyErrors, outerErrors: AllDependencyErrors,
): PkgDependencyErrors { ): PkgDependencyErrors {
const pkgInstalled = pkgs[pkgId].installed const pkg = pkgs[pkgId]
if (!pkgInstalled) return {} if (!isInstalled(pkg)) return {}
return currentDeps(pkgs, pkgId).reduce( return currentDeps(pkgs, pkgId).reduce(
(innerErrors, depId): PkgDependencyErrors => ({ (innerErrors, depId): PkgDependencyErrors => ({
...innerErrors, ...innerErrors,
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors), [depId]: this.getDepError(pkgs, pkg, depId, outerErrors),
}), }),
{} as PkgDependencyErrors, {} as PkgDependencyErrors,
) )
@@ -70,21 +72,21 @@ export class DepErrorService {
private getDepError( private getDepError(
pkgs: DataModel['package-data'], pkgs: DataModel['package-data'],
pkgInstalled: InstalledPackageDataEntry, pkg: PackageDataEntry<InstalledState>,
depId: string, depId: string,
outerErrors: AllDependencyErrors, outerErrors: AllDependencyErrors,
): DependencyError | null { ): DependencyError | null {
const depInstalled = pkgs[depId]?.installed const dep = pkgs[depId]
// not installed // not installed
if (!depInstalled) { if (!dep || dep['state-info'].state !== PackageState.Installed) {
return { return {
type: DependencyErrorType.NotInstalled, type: DependencyErrorType.NotInstalled,
} }
} }
const pkgManifest = pkgInstalled.manifest const pkgManifest = pkg['state-info'].manifest
const depManifest = depInstalled.manifest const depManifest = dep['state-info'].manifest
// incorrect version // incorrect version
if ( if (
@@ -102,16 +104,14 @@ export class DepErrorService {
// invalid config // invalid config
if ( if (
Object.values(pkgInstalled.status['dependency-config-errors']).some( Object.values(pkg.status['dependency-config-errors']).some(err => !!err)
err => !!err,
)
) { ) {
return { return {
type: DependencyErrorType.ConfigUnsatisfied, type: DependencyErrorType.ConfigUnsatisfied,
} }
} }
const depStatus = depInstalled.status.main.status const depStatus = dep.status.main.status
// not running // not running
if ( if (
@@ -125,12 +125,8 @@ export class DepErrorService {
// health check failure // health check failure
if (depStatus === PackageMainStatus.Running) { if (depStatus === PackageMainStatus.Running) {
for (let id of pkgInstalled['current-dependencies'][depId][ for (let id of pkg['current-dependencies'][depId]['health-checks']) {
'health-checks' if (dep.status.main.health[id]?.result !== HealthResult.Success) {
]) {
if (
depInstalled.status.main.health[id]?.result !== HealthResult.Success
) {
return { return {
type: DependencyErrorType.HealthChecksFailed, type: DependencyErrorType.HealthChecksFailed,
} }
@@ -154,9 +150,9 @@ export class DepErrorService {
} }
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] { function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
return Object.keys( return Object.keys(pkgs[id]?.['current-dependencies'] || {}).filter(
pkgs[id]?.installed?.['current-dependencies'] || {}, depId => depId !== id,
).filter(depId => depId !== id) )
} }
function dependencyDepth( function dependencyDepth(

View File

@@ -107,31 +107,11 @@ export enum ServerStatus {
BackingUp = 'backing-up', BackingUp = 'backing-up',
} }
export interface PackageDataEntry { export type PackageDataEntry<T extends StateInfo = StateInfo> = {
state: PackageState 'state-info': T
'static-files': { icon: Url
license: Url
instructions: Url
icon: Url
}
manifest: Manifest
installed?: InstalledPackageDataEntry // exists when: installed, updating
'install-progress'?: InstallProgress // exists when: installing, updating
}
export enum PackageState {
Installing = 'installing',
Installed = 'installed',
Updating = 'updating',
Removing = 'removing',
Restoring = 'restoring',
}
export interface InstalledPackageDataEntry {
status: Status status: Status
manifest: Manifest
'last-backup': string | null 'last-backup': string | null
'system-pointers': any[]
'current-dependents': { [id: string]: CurrentDependencyInfo } 'current-dependents': { [id: string]: CurrentDependencyInfo }
'current-dependencies': { [id: string]: CurrentDependencyInfo } 'current-dependencies': { [id: string]: CurrentDependencyInfo }
'dependency-info': { 'dependency-info': {
@@ -145,6 +125,32 @@ export interface InstalledPackageDataEntry {
'developer-key': string 'developer-key': string
} }
export type StateInfo = InstalledState | InstallingState | UpdatingState
export type InstalledState = {
state: PackageState.Installed | PackageState.Removing
manifest: Manifest
}
export type InstallingState = {
state: PackageState.Installing | PackageState.Restoring
'installing-info': InstallingInfo
}
export type UpdatingState = {
state: PackageState.Updating
'installing-info': InstallingInfo
manifest: Manifest
}
export enum PackageState {
Installing = 'installing',
Installed = 'installed',
Updating = 'updating',
Removing = 'removing',
Restoring = 'restoring',
}
export interface CurrentDependencyInfo { export interface CurrentDependencyInfo {
pointers: any[] pointers: any[]
'health-checks': string[] // array of health check IDs 'health-checks': string[] // array of health check IDs
@@ -354,12 +360,13 @@ export interface HealthCheckResultFailure {
error: string error: string
} }
export interface InstallProgress { export type InstallingInfo = {
readonly size: number | null progress: FullProgress
readonly downloaded: number 'new-manifest': Manifest
readonly 'download-complete': boolean
readonly validated: number
readonly 'validation-complete': boolean
readonly unpacked: number
readonly 'unpack-complete': boolean
} }
export type FullProgress = {
overall: Progress
phases: { name: string; progress: Progress }[]
}
export type Progress = boolean | { done: number; total: number | null } // false means indeterminate. true means complete

View File

@@ -22,15 +22,15 @@ export function renderPkgStatus(
let dependency: DependencyStatus | null = null let dependency: DependencyStatus | null = null
let health: HealthStatus | null = null let health: HealthStatus | null = null
if (pkg.state === PackageState.Installed && pkg.installed) { if (pkg['state-info'].state === PackageState.Installed) {
primary = getPrimaryStatus(pkg.installed.status) primary = getPrimaryStatus(pkg.status)
dependency = getDependencyStatus(depErrors) dependency = getDependencyStatus(depErrors)
health = getHealthStatus( health = getHealthStatus(
pkg.installed.status, pkg.status,
!isEmptyObject(pkg.manifest['health-checks']), !isEmptyObject(pkg['state-info'].manifest['health-checks']),
) )
} else { } else {
primary = pkg.state as string as PrimaryStatus primary = pkg['state-info'].state as string as PrimaryStatus
} }
return { primary, dependency, health } return { primary, dependency, health }

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@angular/core' import { Inject, Injectable } from '@angular/core'
import { WINDOW } from '@ng-web-apis/common' import { WINDOW } from '@ng-web-apis/common'
import { InstalledPackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
@Injectable({ @Injectable({
@@ -12,7 +12,7 @@ export class UiLauncherService {
private readonly config: ConfigService, private readonly config: ConfigService,
) {} ) {}
launch(interfaces: InstalledPackageDataEntry['service-interfaces']): void { launch(interfaces: PackageDataEntry['service-interfaces']): void {
this.windowRef.open( this.windowRef.open(
this.config.launchableAddress(interfaces), this.config.launchableAddress(interfaces),
'_blank', '_blank',

View File

@@ -1,7 +0,0 @@
export interface ProgressData {
totalProgress: number
downloadProgress: number
validateProgress: number
unpackProgress: number
isComplete: boolean
}

View File

@@ -1,5 +1,6 @@
import { Emver } from '@start9labs/shared' import { Emver } from '@start9labs/shared'
import { DataModel } from '../services/patch-db/data-model' import { DataModel } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function dryUpdate( export function dryUpdate(
{ id, version }: { id: string; version: string }, { id, version }: { id: string; version: string },
@@ -9,9 +10,10 @@ export function dryUpdate(
return Object.values(pkgs) return Object.values(pkgs)
.filter( .filter(
pkg => pkg =>
Object.keys(pkg.installed?.['current-dependencies'] || {}).some( Object.keys(pkg['current-dependencies'] || {}).some(
pkgId => pkgId === id, pkgId => pkgId === id,
) && !emver.satisfies(version, pkg.manifest.dependencies[id].version), ) &&
!emver.satisfies(version, getManifest(pkg).dependencies[id].version),
) )
.map(pkg => pkg.manifest.title) .map(pkg => getManifest(pkg).title)
} }

View File

@@ -1,7 +1,12 @@
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
DataModel, DataModel,
InstalledState,
InstallingState,
Manifest,
PackageDataEntry, PackageDataEntry,
PackageState,
UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { firstValueFrom } from 'rxjs' import { firstValueFrom } from 'rxjs'
@@ -17,3 +22,41 @@ export async function getAllPackages(
): Promise<DataModel['package-data']> { ): Promise<DataModel['package-data']> {
return firstValueFrom(patch.watch$('package-data')) return firstValueFrom(patch.watch$('package-data'))
} }
export function getManifest(pkg: PackageDataEntry): Manifest {
if (isInstalled(pkg) || isRemoving(pkg)) return pkg['state-info'].manifest
return (pkg['state-info'] as InstallingState)['installing-info'][
'new-manifest'
]
}
export function isInstalled(
pkg: PackageDataEntry,
): pkg is PackageDataEntry<InstalledState> {
return pkg['state-info'].state === PackageState.Installed
}
export function isRemoving(
pkg: PackageDataEntry,
): pkg is PackageDataEntry<InstalledState> {
return pkg['state-info'].state === PackageState.Removing
}
export function isInstalling(
pkg: PackageDataEntry,
): pkg is PackageDataEntry<InstallingState> {
return pkg['state-info'].state === PackageState.Installing
}
export function isRestoring(
pkg: PackageDataEntry,
): pkg is PackageDataEntry<InstallingState> {
return pkg['state-info'].state === PackageState.Restoring
}
export function isUpdating(
pkg: PackageDataEntry,
): pkg is PackageDataEntry<UpdatingState> {
return pkg['state-info'].state === PackageState.Updating
}

View File

@@ -7,9 +7,7 @@ import {
renderPkgStatus, renderPkgStatus,
StatusRendering, StatusRendering,
} from '../services/pkg-status-rendering.service' } from '../services/pkg-status-rendering.service'
import { ProgressData } from 'src/app/types/progress-data'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { packageLoadingProgress } from './package-loading-progress'
import { PkgDependencyErrors } from '../services/dep-error.service' import { PkgDependencyErrors } from '../services/dep-error.service'
export function getPackageInfo( export function getPackageInfo(
@@ -23,7 +21,6 @@ export function getPackageInfo(
entry, entry,
primaryRendering, primaryRendering,
primaryStatus: statuses.primary, primaryStatus: statuses.primary,
installProgress: packageLoadingProgress(entry['install-progress']),
error: error:
statuses.health === HealthStatus.Failure || statuses.health === HealthStatus.Failure ||
statuses.dependency === DependencyStatus.Warning, statuses.dependency === DependencyStatus.Warning,
@@ -40,7 +37,6 @@ export interface PkgInfo {
entry: PackageDataEntry entry: PackageDataEntry
primaryRendering: StatusRendering primaryRendering: StatusRendering
primaryStatus: PrimaryStatus primaryStatus: PrimaryStatus
installProgress: ProgressData | null
error: boolean error: boolean
warning: boolean warning: boolean
transitioning: boolean transitioning: boolean

View File

@@ -1,7 +1,8 @@
import { PackageDataEntry } from '../services/patch-db/data-model' import { PackageDataEntry } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function hasCurrentDeps(pkg: PackageDataEntry): boolean { export function hasCurrentDeps(pkg: PackageDataEntry): boolean {
return !!Object.keys(pkg.installed?.['current-dependents'] || {}).filter( return !!Object.keys(pkg['current-dependents']).filter(
depId => depId !== pkg.manifest.id, depId => depId !== getManifest(pkg).id,
).length ).length
} }

View File

@@ -1,50 +0,0 @@
import { isEmptyObject } from '@start9labs/shared'
import { ProgressData } from 'src/app/types/progress-data'
import { InstallProgress } from '../services/patch-db/data-model'
export function packageLoadingProgress(
loadData?: InstallProgress,
): ProgressData | null {
if (!loadData || isEmptyObject(loadData)) {
return null
}
let {
downloaded,
validated,
unpacked,
size,
'download-complete': downloadComplete,
'validation-complete': validationComplete,
'unpack-complete': unpackComplete,
} = loadData
// only permit 100% when "complete" == true
size = size || 0
downloaded = downloadComplete ? size : Math.max(downloaded - 1, 0)
validated = validationComplete ? size : Math.max(validated - 1, 0)
unpacked = unpackComplete ? size : Math.max(unpacked - 1, 0)
const downloadWeight = 1
const validateWeight = 0.2
const unpackWeight = 0.7
const numerator = Math.floor(
downloadWeight * downloaded +
validateWeight * validated +
unpackWeight * unpacked,
)
const denominator = Math.floor(
size * (downloadWeight + validateWeight + unpackWeight),
)
const totalProgress = Math.floor((100 * numerator) / denominator)
return {
totalProgress,
downloadProgress: Math.floor((100 * downloaded) / size),
validateProgress: Math.floor((100 * validated) / size),
unpackProgress: Math.floor((100 * unpacked) / size),
isComplete: downloadComplete && validationComplete && unpackComplete,
}
}