Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into integration/new-container-runtime

This commit is contained in:
Aiden McClelland
2024-03-19 11:55:40 -06:00
65 changed files with 1764 additions and 2068 deletions

View File

@@ -240,6 +240,11 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
)
},
Mounts: {
of() {
return Mounts.of<Manifest>()
},
},
HealthCheck: {
of: healthCheck,
},

2
web/package-lock.json generated
View File

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

View File

@@ -20,11 +20,18 @@ import {
} from 'rxjs'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
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 { Emver, THEME } from '@start9labs/shared'
import { ConnectionService } from 'src/app/services/connection.service'
import { ConfigService } from 'src/app/services/config.service'
import {
getManifest,
isInstalled,
isInstalling,
isRestoring,
isUpdating,
} from 'src/app/util/get-package-data'
@Component({
selector: 'app-menu',
@@ -79,8 +86,14 @@ export class MenuComponent {
filter(([prev, curr]) =>
Object.values(prev).some(
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),
@@ -97,9 +110,10 @@ export class MenuComponent {
Object.entries(marketplace).reduce((list, [_, store]) => {
store?.packages.forEach(({ manifest: { id, version } }) => {
if (
local[id] &&
this.emver.compare(
version,
local[id]?.installed?.manifest.version || '',
getManifest(local[id]).version || '',
) === 1
)
list.add(id)

View File

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

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { UnitConversionPipesModule } from '@start9labs/shared'
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({
declarations: [StatusComponent],
@@ -11,7 +11,7 @@ import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/instal
CommonModule,
IonicModule,
UnitConversionPipesModule,
InstallProgressPipeModule,
InstallingProgressPipeModule,
],
exports: [StatusComponent],
})

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
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 {
PrimaryRendering,
PrimaryStatus,
@@ -20,7 +20,7 @@ export class StatusComponent {
@Input() size?: string
@Input() style?: string = 'regular'
@Input() weight?: string = 'normal'
@Input() installProgress?: InstallProgress
@Input() installingInfo?: InstallingInfo
@Input() sigtermTimeout?: string | null = null
readonly connected$ = this.connectionService.connected$

View File

@@ -9,7 +9,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-content class="ion-padding" *ngIf="pkg['state-info'].manifest as manifest">
<!-- loading -->
<text-spinner
*ngIf="loading; else notLoading"
@@ -25,14 +25,14 @@
</ion-item>
<ng-template #noError>
<ng-container *ngIf="configForm && !pkg.installed?.status?.configured">
<ng-container *ngIf="configForm && !pkg.status.configured">
<ng-container *ngIf="!original; else hasOriginal">
<h2
*ngIf="!configForm.dirty"
class="ion-padding-bottom header-details"
>
<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
"Save".
</ion-text>
@@ -59,19 +59,19 @@
<h2 style="display: flex; align-items: center">
<img
style="width: 18px; margin: 4px"
[src]="pkg['static-files'].icon"
[alt]="pkg.manifest.title"
[src]="pkg.icon"
[alt]="manifest.title"
/>
<ion-text
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
>
{{ pkg.manifest.title }}
{{ manifest.title }}
</ion-text>
</h2>
<p>
<ion-text color="dark">
The following modifications have been made to {{
pkg.manifest.title }} to satisfy {{ dependentInfo.title }}:
The following modifications have been made to {{ manifest.title }}
to satisfy {{ dependentInfo.title }}:
<ul>
<li *ngFor="let d of diff" [innerHtml]="d"></li>
</ul>
@@ -85,8 +85,7 @@
<ion-item *ngIf="!hasOptions">
<ion-label>
<p>
No config options for {{ pkg.manifest.title }} {{
pkg.manifest.version }}.
No config options for {{ manifest.title }} {{ manifest.version }}.
</p>
</ion-label>
</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 {
DataModel,
InstalledState,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
@@ -26,7 +27,12 @@ import {
} from 'src/app/services/form.service'
import { compare, Operation, getValueByPointer } from 'fast-json-patch'
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'
@Component({
@@ -39,7 +45,7 @@ export class AppConfigPage {
@Input() dependentInfo?: DependentInfo
pkg!: PackageDataEntry
pkg!: PackageDataEntry<InstalledState>
loadingText = ''
configSpec?: ConfigSpec
@@ -68,10 +74,11 @@ export class AppConfigPage {
async ngOnInit() {
try {
const pkg = await getPackage(this.patch, this.pkgId)
if (!pkg) return
if (!pkg || !isInstalled(pkg)) return
this.pkg = pkg
if (!this.pkg.manifest.config) return
if (!this.pkg['state-info'].manifest.config) return
let newConfig: object | 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>'
const localPkgs = await getAllPackages(this.patch)
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>`
})
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 { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs'
import { getManifest } from 'src/app/util/get-package-data'
@Component({
selector: 'backup-select',
@@ -32,13 +33,13 @@ export class BackupSelectPage {
map(pkgs => {
return Object.values(pkgs)
.map(pkg => {
const { id, title } = pkg.manifest
const { id, title } = getManifest(pkg)
return {
id,
title,
icon: pkg['static-files'].icon,
disabled: pkg.state !== PackageState.Installed,
checked: pkg.state === PackageState.Installed,
icon: pkg.icon,
disabled: pkg['state-info'].state !== PackageState.Installed,
checked: false,
}
})
.sort((a, b) =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ export class AppPropertiesPage {
unmasked: { [key: string]: boolean } = {}
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))
@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 { ToHealthChecksPipe } from './pipes/to-health-checks.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 = [
{
@@ -31,7 +31,6 @@ const routes: Routes = [
declarations: [
AppShowPage,
HealthColorPipe,
ProgressDataPipe,
ToHealthChecksPipe,
ToButtonsPipe,
AppShowHeaderComponent,
@@ -44,7 +43,7 @@ const routes: Routes = [
],
imports: [
CommonModule,
StatusComponentModule,
InstallingProgressPipeModule,
IonicModule,
RouterModule.forChild(routes),
AppConfigPageModule,
@@ -52,6 +51,7 @@ const routes: Routes = [
LaunchablePipeModule,
UiPipeModule,
ResponsiveColModule,
StatusComponentModule,
],
})
export class AppShowPageModule {}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { ModalController, ToastController } from '@ionic/angular'
import { copyToClipboard, MarkdownComponent } from '@start9labs/shared'
import { from } from 'rxjs'
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({
selector: 'app-show-additional',
@@ -12,7 +12,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
})
export class AppShowAdditionalComponent {
@Input()
pkg!: PackageDataEntry
manifest!: Manifest
constructor(
private readonly modalCtrl: ModalController,
@@ -35,10 +35,16 @@ export class AppShowAdditionalComponent {
}
async presentModalLicense() {
const { id, version } = this.manifest
const modal = await this.modalCtrl.create({
componentProps: {
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,
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import {
InstallProgress,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { ProgressData } from 'src/app/types/progress-data'
import { FullProgress } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'app-show-progress',
@@ -13,26 +9,5 @@ import { ProgressData } from 'src/app/types/progress-data'
})
export class AppShowProgressComponent {
@Input()
pkg!: PackageDataEntry
@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'
}
phases!: FullProgress['phases']
}

View File

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

View File

@@ -6,7 +6,7 @@ import {
PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service'
import {
InstalledPackageDataEntry,
Manifest,
PackageDataEntry,
PackageMainStatus,
PackageState,
@@ -18,6 +18,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ModalService } from 'src/app/services/modal.service'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { ConnectionService } from 'src/app/services/connection.service'
import { isInstalled, getManifest } from 'src/app/util/get-package-data'
@Component({
selector: 'app-show-status',
@@ -34,6 +35,8 @@ export class AppShowStatusComponent {
PR = PrimaryRendering
isInstalled = isInstalled
readonly connected$ = this.connectionService.connected$
constructor(
@@ -46,18 +49,16 @@ export class AppShowStatusComponent {
private readonly connectionService: ConnectionService,
) {}
get interfaces():
| InstalledPackageDataEntry['service-interfaces']
| undefined {
return this.pkg.installed?.['service-interfaces']
get interfaces(): PackageDataEntry['service-interfaces'] {
return this.pkg['service-interfaces']
}
get pkgStatus(): Status | null {
return this.pkg.installed?.status || null
get pkgStatus(): Status {
return this.pkg.status
}
get isInstalled(): boolean {
return this.pkg.state === PackageState.Installed
get manifest(): Manifest {
return getManifest(this.pkg)
}
get isRunning(): boolean {
@@ -82,25 +83,25 @@ export class AppShowStatusComponent {
: null
}
launchUi(interfaces: InstalledPackageDataEntry['service-interfaces']): void {
launchUi(interfaces: PackageDataEntry['service-interfaces']): void {
this.launcherService.launch(interfaces)
}
async presentModalConfig(): Promise<void> {
return this.modalService.presentModalConfig({
pkgId: this.id,
pkgId: this.manifest.id,
})
}
async tryStart(): Promise<void> {
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)
if (!proceed) return
}
const alertMsg = this.pkg.manifest.alerts.start
const alertMsg = this.manifest.alerts.start
if (alertMsg) {
const proceed = await this.presentAlertStart(alertMsg)
@@ -112,7 +113,7 @@ export class AppShowStatusComponent {
}
async tryStop(): Promise<void> {
const { title, alerts } = this.pkg.manifest
const { title, alerts } = this.manifest
let message = alerts.stop || ''
if (hasCurrentDeps(this.pkg)) {
@@ -150,7 +151,7 @@ export class AppShowStatusComponent {
if (hasCurrentDeps(this.pkg)) {
const alert = await this.alertCtrl.create({
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: [
{
text: 'Cancel',
@@ -173,10 +174,6 @@ export class AppShowStatusComponent {
}
}
private get id(): string {
return this.pkg.manifest.id
}
private async start(): Promise<void> {
const loader = await this.loadingCtrl.create({
message: `Starting...`,
@@ -184,7 +181,7 @@ export class AppShowStatusComponent {
await loader.present()
try {
await this.embassyApi.startPackage({ id: this.id })
await this.embassyApi.startPackage({ id: this.manifest.id })
} catch (e: any) {
this.errToast.present(e)
} finally {
@@ -199,7 +196,7 @@ export class AppShowStatusComponent {
await loader.present()
try {
await this.embassyApi.stopPackage({ id: this.id })
await this.embassyApi.stopPackage({ id: this.manifest.id })
} catch (e: any) {
this.errToast.present(e)
} finally {
@@ -214,7 +211,7 @@ export class AppShowStatusComponent {
await loader.present()
try {
await this.embassyApi.restartPackage({ id: this.id })
await this.embassyApi.restartPackage({ id: this.manifest.id })
} catch (e: any) {
this.errToast.present(e)
} 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 {
DataModel,
InstalledState,
Manifest,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { ModalService } from 'src/app/services/modal.service'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { from, map, Observable } from 'rxjs'
import { PatchDB } from 'patch-db-client'
import { getManifest } from 'src/app/util/get-package-data'
export interface Button {
title: string
@@ -33,26 +36,26 @@ export class ToButtonsPipe implements PipeTransform {
private readonly patch: PatchDB<DataModel>,
) {}
transform(pkg: PackageDataEntry): Button[] {
const pkgTitle = pkg.manifest.title
transform(pkg: PackageDataEntry<InstalledState>): Button[] {
const manifest = pkg['state-info'].manifest
return [
// instructions
{
action: () => this.presentModalInstructions(pkg),
action: () => this.presentModalInstructions(manifest),
title: 'Instructions',
description: `Understand how to use ${pkgTitle}`,
description: `Understand how to use ${manifest.title}`,
icon: 'list-outline',
highlighted$: this.patch
.watch$('ui', 'ack-instructions', pkg.manifest.id)
.watch$('ui', 'ack-instructions', manifest.id)
.pipe(map(seen => !seen)),
},
// config
{
action: async () =>
this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }),
this.modalService.presentModalConfig({ pkgId: manifest.id }),
title: 'Config',
description: `Customize ${pkgTitle}`,
description: `Customize ${manifest.title}`,
icon: 'options-outline',
},
// properties
@@ -71,7 +74,7 @@ export class ToButtonsPipe implements PipeTransform {
action: () =>
this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
title: 'Actions',
description: `Uninstall and other commands specific to ${pkgTitle}`,
description: `Uninstall and other commands specific to ${manifest.title}`,
icon: 'flash-outline',
},
// interfaces
@@ -97,16 +100,18 @@ export class ToButtonsPipe implements PipeTransform {
]
}
private async presentModalInstructions(pkg: PackageDataEntry) {
private async presentModalInstructions(manifest: Manifest) {
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))
const modal = await this.modalCtrl.create({
componentProps: {
title: 'Instructions',
content: from(
this.apiService.getStatic(pkg['static-files']['instructions']),
this.apiService.getStatic(
`/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`,
),
),
},
component: MarkdownComponent,
@@ -115,17 +120,22 @@ export class ToButtonsPipe implements PipeTransform {
await modal.present()
}
private viewInMarketplaceButton(pkg: PackageDataEntry): Button {
const url = pkg.installed?.['marketplace-url']
private viewInMarketplaceButton(
pkg: PackageDataEntry<InstalledState>,
): Button {
const url = pkg['marketplace-url']
const queryParams = url ? { url } : {}
let button: Button = {
title: 'Marketplace Listing',
icon: 'storefront-outline',
action: () =>
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
queryParams,
}),
this.navCtrl.navigateForward(
[`marketplace/${pkg['state-info'].manifest.id}`],
{
queryParams,
},
),
disabled: false,
description: 'View service in the marketplace',
}

View File

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

View File

@@ -8,7 +8,7 @@
View Installed
</ion-button>
<ng-container *ngIf="localPkg; else install">
<ng-container *ngIf="localPkg.state === PackageState.Installed">
<ng-container *ngIf="localPkg['state-info'].state === 'installed'">
<ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
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 { hasCurrentDeps } from 'src/app/util/has-deps'
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 { dryUpdate } from 'src/app/util/dry-update'
@@ -46,8 +46,6 @@ export class MarketplaceShowControlsComponent {
readonly showDevTools$ = this.ClientStorageService.showDevTools$
readonly PackageState = PackageState
constructor(
private readonly alertCtrl: AlertController,
private readonly ClientStorageService: ClientStorageService,
@@ -60,7 +58,7 @@ export class MarketplaceShowControlsComponent {
) {}
get localVersion(): string {
return this.localPkg?.manifest.version || ''
return this.localPkg ? getManifest(this.localPkg).version : ''
}
async tryInstall() {
@@ -72,7 +70,7 @@ export class MarketplaceShowControlsComponent {
if (!this.localPkg) {
this.alertInstall(url)
} else {
const originalUrl = this.localPkg.installed?.['marketplace-url']
const originalUrl = this.localPkg['marketplace-url']
if (!sameUrl(url, originalUrl)) {
const proceed = await this.presentAlertDifferentMarketplace(

View File

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

View File

@@ -1,8 +1,13 @@
import { Component, Input } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import {
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
isInstalled,
isInstalling,
isUpdating,
isRemoving,
isRestoring,
getManifest,
} from 'src/app/util/get-package-data'
@Component({
selector: 'marketplace-status',
@@ -14,9 +19,13 @@ export class MarketplaceStatusComponent {
@Input() localPkg?: PackageDataEntry
PackageState = PackageState
isInstalled = isInstalled
isInstalling = isInstalling
isUpdating = isUpdating
isRemoving = isRemoving
isRestoring = isRestoring
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 { IonicModule } from '@ionic/angular'
import { EmverPipesModule } from '@start9labs/shared'
import { InstallProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
import { MarketplaceStatusComponent } from './marketplace-status.component'
@NgModule({
@@ -11,7 +10,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component'
CommonModule,
IonicModule,
EmverPipesModule,
InstallProgressPipeModule,
InstallingProgressPipeModule,
],
declarations: [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 { SharedPipesModule } from '@start9labs/shared'
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
const routes: Routes = [
{
@@ -22,6 +23,7 @@ const routes: Routes = [
BadgeMenuComponentModule,
SharedPipesModule,
BackupReportPageModule,
UiPipeModule,
],
declarations: [NotificationsPage],
})

View File

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

View File

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

View File

@@ -25,8 +25,6 @@ export class BackingUpComponent {
'backup-progress',
)
PackageMainStatus = PackageMainStatus
constructor(private readonly patch: PatchDB<DataModel>) {}
}
@@ -35,14 +33,7 @@ export class BackingUpComponent {
})
export class PkgMainStatusPipe implements PipeTransform {
transform(pkgId: string): Observable<PackageMainStatus> {
return this.patch.watch$(
'package-data',
pkgId,
'installed',
'status',
'main',
'status',
)
return this.patch.watch$('package-data', pkgId, 'status', 'main', 'status')
}
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 { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module'
import { PkgMainStatusPipe } from './backing-up/backing-up.component'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
const routes: Routes = [
{
@@ -24,6 +25,7 @@ const routes: Routes = [
SharedPipesModule,
BackupDrivesComponentModule,
BackupSelectPageModule,
UiPipeModule,
],
declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe],
})

View File

@@ -12,7 +12,7 @@ import {
} from '@start9labs/shared'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
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 { MimeTypePipeModule } from '@start9labs/marketplace'
@@ -34,7 +34,7 @@ const routes: Routes = [
SkeletonListComponentModule,
MarkdownPipeModule,
RoundProgressModule,
InstallProgressPipeModule,
InstallingProgressPipeModule,
StoreIconComponentModule,
EmverPipesModule,
MimeTypePipeModule,

View File

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

View File

@@ -2,7 +2,9 @@ import { Component, Inject } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import {
@@ -14,16 +16,20 @@ import {
} from '@start9labs/marketplace'
import { Emver, isEmptyObject } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core'
import { combineLatest, Observable } from 'rxjs'
import { combineLatest, map, Observable } from 'rxjs'
import { AlertController, NavController } from '@ionic/angular'
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'
interface UpdatesData {
hosts: StoreIdentity[]
marketplace: Marketplace
localPkgs: Record<string, PackageDataEntry>
localPkgs: Record<string, PackageDataEntry<InstalledState | UpdatingState>>
errors: string[]
}
@@ -36,7 +42,14 @@ export class UpdatesPage {
readonly data$: Observable<UpdatesData> = combineLatest({
hosts: this.marketplaceService.getKnownHosts$(true),
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$(),
})
@@ -154,14 +167,17 @@ export class FilterUpdatesPipe implements PipeTransform {
transform(
pkgs: MarketplacePkg[],
local: Record<string, PackageDataEntry | undefined>,
local: Record<string, PackageDataEntry<InstalledState | UpdatingState>>,
): MarketplacePkg[] {
return pkgs.filter(
({ manifest }) =>
return pkgs.filter(({ manifest }) => {
const localPkg = local[manifest.id]
return (
localPkg &&
this.emver.compare(
manifest.version,
local[manifest.id]?.installed?.manifest.version || '',
) === 1,
)
localPkg['state-info'].manifest.version,
) === 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 { combineLatest } from 'rxjs'
import { DepErrorService } from 'src/app/services/dep-error.service'
import { getManifest } from 'src/app/util/get-package-data'
@Component({
selector: 'widget-health',
@@ -31,7 +32,7 @@ export class HealthComponent {
]).pipe(
map(([data, depErrors]) => {
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>>(
(acc, label) => ({

View File

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

View File

@@ -1,24 +1,32 @@
import { Pipe, PipeTransform } from '@angular/core'
import { InstallProgress } from 'src/app/services/patch-db/data-model'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
import { Progress } from 'src/app/services/patch-db/data-model'
@Pipe({
name: 'installProgress',
name: 'installingProgressString',
})
export class InstallProgressPipe implements PipeTransform {
transform(installProgress?: InstallProgress): number {
return packageLoadingProgress(installProgress)?.totalProgress || 0
export class InstallingProgressDisplayPipe implements PipeTransform {
transform(progress: Progress): string {
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({
name: 'installProgressDisplay',
name: 'installingProgress',
})
export class InstallProgressDisplayPipe implements PipeTransform {
transform(installProgress?: InstallProgress): string {
const totalProgress =
packageLoadingProgress(installProgress)?.totalProgress || 0
return totalProgress < 99 ? totalProgress + '%' : 'finalizing'
export class InstallingProgressPipe implements PipeTransform {
transform(progress: Progress): number | null {
if (progress === true) return 1
if (progress === false || !progress.total) return null
return Number((progress.done / progress.total).toFixed(2))
}
}
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 { UiPipe } from './ui.pipe'
import { ToManifestPipe, UiPipe } from './ui.pipe'
@NgModule({
declarations: [UiPipe],
exports: [UiPipe],
declarations: [UiPipe, ToManifestPipe],
exports: [UiPipe, ToManifestPipe],
})
export class UiPipeModule {}

View File

@@ -1,14 +1,22 @@
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 { getManifest } from 'src/app/util/get-package-data'
@Pipe({
name: 'hasUi',
})
export class UiPipe implements PipeTransform {
transform(
interfaces: InstalledPackageDataEntry['service-interfaces'],
): boolean {
transform(interfaces: PackageDataEntry['service-interfaces']): boolean {
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'
import {
DataModel,
InstallProgress,
FullProgress,
InstallingState,
PackageDataEntry,
PackageMainStatus,
PackageState,
ServerStatus,
StateInfo,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { CifsBackupTarget, RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
@@ -39,14 +42,34 @@ import { AuthService } from '../auth.service'
import { ConnectionService } from '../connection.service'
import { StoreInfo } from '@start9labs/marketplace'
const PROGRESS: InstallProgress = {
size: 120,
downloaded: 0,
'download-complete': false,
validated: 0,
'validation-complete': false,
unpacked: 0,
'unpack-complete': false,
const PROGRESS: FullProgress = {
overall: {
done: 0,
total: 120,
},
phases: [
{
name: 'Downloading',
progress: {
done: 0,
total: 40,
},
},
{
name: 'Validating',
progress: {
done: 0,
total: 40,
},
},
{
name: 'Installing',
progress: {
done: 0,
total: 40,
},
},
],
}
@Injectable()
@@ -656,15 +679,30 @@ export class MockApiService extends ApiService {
this.updateProgress(params.id)
}, 1000)
const patch: Operation<PackageDataEntry>[] = [
const manifest = Mock.LocalPkgs[params.id]['state-info'].manifest
const patch: Operation<
PackageDataEntry<InstallingState | UpdatingState>
>[] = [
{
op: PatchOp.ADD,
path: `/package-data/${params.id}`,
value: {
...Mock.LocalPkgs[params.id],
// state: PackageState.Installing,
state: PackageState.Updating,
'install-progress': { ...PROGRESS },
'state-info': {
// if installing
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}`,
value: {
...Mock.LocalPkgs[id],
state: PackageState.Restoring,
'install-progress': { ...PROGRESS },
installed: undefined,
'state-info': {
state: PackageState.Restoring,
'installing-info': {
'new-manifest': Mock.LocalPkgs[id]['state-info'].manifest!,
progress: PROGRESS,
},
},
},
}
})
@@ -948,7 +990,7 @@ export class MockApiService extends ApiService {
const patch = [
{
op: PatchOp.REPLACE,
path: `/package-data/${params.id}/state`,
path: `/package-data/${params.id}/state-info/state`,
value: PackageState.Removing,
},
]
@@ -977,55 +1019,100 @@ export class MockApiService extends ApiService {
}
private async updateProgress(id: string): Promise<void> {
const progress = { ...PROGRESS }
const phases = [
{ progress: 'downloaded', completion: 'download-complete' },
{ progress: 'validated', completion: 'validation-complete' },
{ progress: 'unpacked', completion: 'unpack-complete' },
] as const
const progress = JSON.parse(JSON.stringify(PROGRESS))
for (let phase of phases) {
let i = progress[phase.progress]
const size = progress?.size || 0
while (i < size) {
await pauseFor(250)
i = Math.min(i + 5, size)
progress[phase.progress] = i
for (let [i, phase] of progress.phases.entries()) {
if (typeof phase.progress !== 'object' || !phase.progress.total) {
await pauseFor(2000)
if (i === progress.size) {
progress[phase.completion] = true
}
const patch = [
const patches: Operation<any>[] = [
{
op: PatchOp.REPLACE,
path: `/package-data/${id}/install-progress`,
value: { ...progress },
path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/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(() => {
const patch2: Operation<any>[] = [
{
op: PatchOp.REPLACE,
path: `/package-data/${id}/state`,
value: PackageState.Installed,
await pauseFor(1000)
this.mockRevision([
{
op: PatchOp.REPLACE,
path: `/package-data/${id}/state-info/installing-info/progress/overall`,
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`,
value: { ...Mock.LocalPkgs[id].installed },
},
{
op: PatchOp.REMOVE,
path: `/package-data/${id}/install-progress`,
},
]
this.mockRevision(patch2)
}, 1000)
},
]
this.mockRevision(patch2)
}
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 { types } from '@start9labs/start-sdk'
import {
InstalledPackageDataEntry,
PackageDataEntry,
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
@@ -64,7 +64,7 @@ export class ConfigService {
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
launchableAddress(
interfaces: InstalledPackageDataEntry['service-interfaces'],
interfaces: PackageDataEntry['service-interfaces'],
): string {
const ui = Object.values(interfaces).find(i => i.type === 'ui')
@@ -128,7 +128,7 @@ export class ConfigService {
}
export function hasUi(
interfaces: InstalledPackageDataEntry['service-interfaces'],
interfaces: PackageDataEntry['service-interfaces'],
): boolean {
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 {
DataModel,
HealthCheckResult,
HealthResult,
InstalledPackageDataEntry,
InstalledState,
PackageDataEntry,
PackageMainStatus,
PackageState,
} from './patch-db/data-model'
import * as deepEqual from 'fast-deep-equal'
import { isInstalled } from '../util/get-package-data'
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
export type PkgDependencyErrors = Record<string, DependencyError | null>
@@ -55,14 +57,14 @@ export class DepErrorService {
pkgId: string,
outerErrors: AllDependencyErrors,
): PkgDependencyErrors {
const pkgInstalled = pkgs[pkgId].installed
const pkg = pkgs[pkgId]
if (!pkgInstalled) return {}
if (!isInstalled(pkg)) return {}
return currentDeps(pkgs, pkgId).reduce(
(innerErrors, depId): PkgDependencyErrors => ({
...innerErrors,
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
[depId]: this.getDepError(pkgs, pkg, depId, outerErrors),
}),
{} as PkgDependencyErrors,
)
@@ -70,21 +72,21 @@ export class DepErrorService {
private getDepError(
pkgs: DataModel['package-data'],
pkgInstalled: InstalledPackageDataEntry,
pkg: PackageDataEntry<InstalledState>,
depId: string,
outerErrors: AllDependencyErrors,
): DependencyError | null {
const depInstalled = pkgs[depId]?.installed
const dep = pkgs[depId]
// not installed
if (!depInstalled) {
if (!dep || dep['state-info'].state !== PackageState.Installed) {
return {
type: DependencyErrorType.NotInstalled,
}
}
const pkgManifest = pkgInstalled.manifest
const depManifest = depInstalled.manifest
const pkgManifest = pkg['state-info'].manifest
const depManifest = dep['state-info'].manifest
// incorrect version
if (
@@ -102,16 +104,14 @@ export class DepErrorService {
// invalid config
if (
Object.values(pkgInstalled.status['dependency-config-errors']).some(
err => !!err,
)
Object.values(pkg.status['dependency-config-errors']).some(err => !!err)
) {
return {
type: DependencyErrorType.ConfigUnsatisfied,
}
}
const depStatus = depInstalled.status.main.status
const depStatus = dep.status.main.status
// not running
if (
@@ -125,12 +125,8 @@ export class DepErrorService {
// health check failure
if (depStatus === PackageMainStatus.Running) {
for (let id of pkgInstalled['current-dependencies'][depId][
'health-checks'
]) {
if (
depInstalled.status.main.health[id]?.result !== HealthResult.Success
) {
for (let id of pkg['current-dependencies'][depId]['health-checks']) {
if (dep.status.main.health[id]?.result !== HealthResult.Success) {
return {
type: DependencyErrorType.HealthChecksFailed,
}
@@ -154,9 +150,9 @@ export class DepErrorService {
}
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
return Object.keys(
pkgs[id]?.installed?.['current-dependencies'] || {},
).filter(depId => depId !== id)
return Object.keys(pkgs[id]?.['current-dependencies'] || {}).filter(
depId => depId !== id,
)
}
function dependencyDepth(

View File

@@ -107,31 +107,11 @@ export enum ServerStatus {
BackingUp = 'backing-up',
}
export interface PackageDataEntry {
state: PackageState
'static-files': {
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 {
export type PackageDataEntry<T extends StateInfo = StateInfo> = {
'state-info': T
icon: Url
status: Status
manifest: Manifest
'last-backup': string | null
'system-pointers': any[]
'current-dependents': { [id: string]: CurrentDependencyInfo }
'current-dependencies': { [id: string]: CurrentDependencyInfo }
'dependency-info': {
@@ -145,6 +125,32 @@ export interface InstalledPackageDataEntry {
'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 {
pointers: any[]
'health-checks': string[] // array of health check IDs
@@ -354,12 +360,13 @@ export interface HealthCheckResultFailure {
error: string
}
export interface InstallProgress {
readonly size: number | null
readonly downloaded: number
readonly 'download-complete': boolean
readonly validated: number
readonly 'validation-complete': boolean
readonly unpacked: number
readonly 'unpack-complete': boolean
export type InstallingInfo = {
progress: FullProgress
'new-manifest': Manifest
}
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 health: HealthStatus | null = null
if (pkg.state === PackageState.Installed && pkg.installed) {
primary = getPrimaryStatus(pkg.installed.status)
if (pkg['state-info'].state === PackageState.Installed) {
primary = getPrimaryStatus(pkg.status)
dependency = getDependencyStatus(depErrors)
health = getHealthStatus(
pkg.installed.status,
!isEmptyObject(pkg.manifest['health-checks']),
pkg.status,
!isEmptyObject(pkg['state-info'].manifest['health-checks']),
)
} else {
primary = pkg.state as string as PrimaryStatus
primary = pkg['state-info'].state as string as PrimaryStatus
}
return { primary, dependency, health }

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@angular/core'
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'
@Injectable({
@@ -12,7 +12,7 @@ export class UiLauncherService {
private readonly config: ConfigService,
) {}
launch(interfaces: InstalledPackageDataEntry['service-interfaces']): void {
launch(interfaces: PackageDataEntry['service-interfaces']): void {
this.windowRef.open(
this.config.launchableAddress(interfaces),
'_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 { DataModel } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function dryUpdate(
{ id, version }: { id: string; version: string },
@@ -9,9 +10,10 @@ export function dryUpdate(
return Object.values(pkgs)
.filter(
pkg =>
Object.keys(pkg.installed?.['current-dependencies'] || {}).some(
Object.keys(pkg['current-dependencies'] || {}).some(
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 {
DataModel,
InstalledState,
InstallingState,
Manifest,
PackageDataEntry,
PackageState,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { firstValueFrom } from 'rxjs'
@@ -17,3 +22,41 @@ export async function getAllPackages(
): Promise<DataModel['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,
StatusRendering,
} from '../services/pkg-status-rendering.service'
import { ProgressData } from 'src/app/types/progress-data'
import { Subscription } from 'rxjs'
import { packageLoadingProgress } from './package-loading-progress'
import { PkgDependencyErrors } from '../services/dep-error.service'
export function getPackageInfo(
@@ -23,7 +21,6 @@ export function getPackageInfo(
entry,
primaryRendering,
primaryStatus: statuses.primary,
installProgress: packageLoadingProgress(entry['install-progress']),
error:
statuses.health === HealthStatus.Failure ||
statuses.dependency === DependencyStatus.Warning,
@@ -40,7 +37,6 @@ export interface PkgInfo {
entry: PackageDataEntry
primaryRendering: StatusRendering
primaryStatus: PrimaryStatus
installProgress: ProgressData | null
error: boolean
warning: boolean
transitioning: boolean

View File

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