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

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) => ({