mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 04:01:58 +00:00
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:
2
web/package-lock.json
generated
2
web/package-lock.json
generated
@@ -1973,7 +1973,7 @@
|
|||||||
},
|
},
|
||||||
"../sdk/dist": {
|
"../sdk/dist": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-rev0.lib0.rc8.beta7",
|
"version": "0.4.0-rev0.lib0.rc8.beta10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
|
|||||||
@@ -20,11 +20,18 @@ import {
|
|||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||||
import { Emver, THEME } from '@start9labs/shared'
|
import { Emver, THEME } from '@start9labs/shared'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import {
|
||||||
|
getManifest,
|
||||||
|
isInstalled,
|
||||||
|
isInstalling,
|
||||||
|
isRestoring,
|
||||||
|
isUpdating,
|
||||||
|
} from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-menu',
|
selector: 'app-menu',
|
||||||
@@ -79,8 +86,14 @@ export class MenuComponent {
|
|||||||
filter(([prev, curr]) =>
|
filter(([prev, curr]) =>
|
||||||
Object.values(prev).some(
|
Object.values(prev).some(
|
||||||
p =>
|
p =>
|
||||||
p['install-progress'] &&
|
[
|
||||||
!curr[p.manifest.id]?.['install-progress'],
|
PackageState.Installing,
|
||||||
|
PackageState.Updating,
|
||||||
|
PackageState.Restoring,
|
||||||
|
].includes(p['state-info'].state) &&
|
||||||
|
[PackageState.Installed, PackageState.Removing].includes(
|
||||||
|
curr[getManifest(p).id]['state-info'].state,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
map(([_, curr]) => curr),
|
map(([_, curr]) => curr),
|
||||||
@@ -97,9 +110,10 @@ export class MenuComponent {
|
|||||||
Object.entries(marketplace).reduce((list, [_, store]) => {
|
Object.entries(marketplace).reduce((list, [_, store]) => {
|
||||||
store?.packages.forEach(({ manifest: { id, version } }) => {
|
store?.packages.forEach(({ manifest: { id, version } }) => {
|
||||||
if (
|
if (
|
||||||
|
local[id] &&
|
||||||
this.emver.compare(
|
this.emver.compare(
|
||||||
version,
|
version,
|
||||||
local[id]?.installed?.manifest.version || '',
|
getManifest(local[id]).version || '',
|
||||||
) === 1
|
) === 1
|
||||||
)
|
)
|
||||||
list.add(id)
|
list.add(id)
|
||||||
|
|||||||
@@ -12,12 +12,9 @@
|
|||||||
. This may take a while
|
. This may take a while
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span *ngIf="installProgress">
|
<span *ngIf="installingInfo">
|
||||||
<ion-text
|
<ion-text color="primary">
|
||||||
*ngIf="installProgress | installProgressDisplay as progress"
|
{{ installingInfo.progress.overall | installingProgressString }}
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{{ progress }}
|
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { UnitConversionPipesModule } from '@start9labs/shared'
|
import { UnitConversionPipesModule } from '@start9labs/shared'
|
||||||
import { StatusComponent } from './status.component'
|
import { StatusComponent } from './status.component'
|
||||||
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [StatusComponent],
|
declarations: [StatusComponent],
|
||||||
@@ -11,7 +11,7 @@ import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/instal
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
UnitConversionPipesModule,
|
UnitConversionPipesModule,
|
||||||
InstallProgressPipeModule,
|
InstallingProgressPipeModule,
|
||||||
],
|
],
|
||||||
exports: [StatusComponent],
|
exports: [StatusComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import {
|
import {
|
||||||
PrimaryRendering,
|
PrimaryRendering,
|
||||||
PrimaryStatus,
|
PrimaryStatus,
|
||||||
@@ -20,7 +20,7 @@ export class StatusComponent {
|
|||||||
@Input() size?: string
|
@Input() size?: string
|
||||||
@Input() style?: string = 'regular'
|
@Input() style?: string = 'regular'
|
||||||
@Input() weight?: string = 'normal'
|
@Input() weight?: string = 'normal'
|
||||||
@Input() installProgress?: InstallProgress
|
@Input() installingInfo?: InstallingInfo
|
||||||
@Input() sigtermTimeout?: string | null = null
|
@Input() sigtermTimeout?: string | null = null
|
||||||
|
|
||||||
readonly connected$ = this.connectionService.connected$
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding" *ngIf="pkg['state-info'].manifest as manifest">
|
||||||
<!-- loading -->
|
<!-- loading -->
|
||||||
<text-spinner
|
<text-spinner
|
||||||
*ngIf="loading; else notLoading"
|
*ngIf="loading; else notLoading"
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-template #noError>
|
<ng-template #noError>
|
||||||
<ng-container *ngIf="configForm && !pkg.installed?.status?.configured">
|
<ng-container *ngIf="configForm && !pkg.status.configured">
|
||||||
<ng-container *ngIf="!original; else hasOriginal">
|
<ng-container *ngIf="!original; else hasOriginal">
|
||||||
<h2
|
<h2
|
||||||
*ngIf="!configForm.dirty"
|
*ngIf="!configForm.dirty"
|
||||||
class="ion-padding-bottom header-details"
|
class="ion-padding-bottom header-details"
|
||||||
>
|
>
|
||||||
<ion-text color="success">
|
<ion-text color="success">
|
||||||
{{ pkg.manifest.title }} has been automatically configured with
|
{{ manifest.title }} has been automatically configured with
|
||||||
recommended defaults. Make whatever changes you want, then click
|
recommended defaults. Make whatever changes you want, then click
|
||||||
"Save".
|
"Save".
|
||||||
</ion-text>
|
</ion-text>
|
||||||
@@ -59,19 +59,19 @@
|
|||||||
<h2 style="display: flex; align-items: center">
|
<h2 style="display: flex; align-items: center">
|
||||||
<img
|
<img
|
||||||
style="width: 18px; margin: 4px"
|
style="width: 18px; margin: 4px"
|
||||||
[src]="pkg['static-files'].icon"
|
[src]="pkg.icon"
|
||||||
[alt]="pkg.manifest.title"
|
[alt]="manifest.title"
|
||||||
/>
|
/>
|
||||||
<ion-text
|
<ion-text
|
||||||
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
|
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
|
||||||
>
|
>
|
||||||
{{ pkg.manifest.title }}
|
{{ manifest.title }}
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
<ion-text color="dark">
|
<ion-text color="dark">
|
||||||
The following modifications have been made to {{
|
The following modifications have been made to {{ manifest.title }}
|
||||||
pkg.manifest.title }} to satisfy {{ dependentInfo.title }}:
|
to satisfy {{ dependentInfo.title }}:
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let d of diff" [innerHtml]="d"></li>
|
<li *ngFor="let d of diff" [innerHtml]="d"></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -85,8 +85,7 @@
|
|||||||
<ion-item *ngIf="!hasOptions">
|
<ion-item *ngIf="!hasOptions">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p>
|
<p>
|
||||||
No config options for {{ pkg.manifest.title }} {{
|
No config options for {{ manifest.title }} {{ manifest.version }}.
|
||||||
pkg.manifest.version }}.
|
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { DependentInfo } from 'src/app/types/dependent-info'
|
|||||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
|
InstalledState,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
@@ -26,7 +27,12 @@ import {
|
|||||||
} from 'src/app/services/form.service'
|
} from 'src/app/services/form.service'
|
||||||
import { compare, Operation, getValueByPointer } from 'fast-json-patch'
|
import { compare, Operation, getValueByPointer } from 'fast-json-patch'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||||
import { getAllPackages, getPackage } from 'src/app/util/get-package-data'
|
import {
|
||||||
|
getAllPackages,
|
||||||
|
getManifest,
|
||||||
|
getPackage,
|
||||||
|
isInstalled,
|
||||||
|
} from 'src/app/util/get-package-data'
|
||||||
import { Breakages } from 'src/app/services/api/api.types'
|
import { Breakages } from 'src/app/services/api/api.types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -39,7 +45,7 @@ export class AppConfigPage {
|
|||||||
|
|
||||||
@Input() dependentInfo?: DependentInfo
|
@Input() dependentInfo?: DependentInfo
|
||||||
|
|
||||||
pkg!: PackageDataEntry
|
pkg!: PackageDataEntry<InstalledState>
|
||||||
loadingText = ''
|
loadingText = ''
|
||||||
|
|
||||||
configSpec?: ConfigSpec
|
configSpec?: ConfigSpec
|
||||||
@@ -68,10 +74,11 @@ export class AppConfigPage {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
const pkg = await getPackage(this.patch, this.pkgId)
|
const pkg = await getPackage(this.patch, this.pkgId)
|
||||||
if (!pkg) return
|
if (!pkg || !isInstalled(pkg)) return
|
||||||
|
|
||||||
this.pkg = pkg
|
this.pkg = pkg
|
||||||
|
|
||||||
if (!this.pkg.manifest.config) return
|
if (!this.pkg['state-info'].manifest.config) return
|
||||||
|
|
||||||
let newConfig: object | undefined
|
let newConfig: object | undefined
|
||||||
let patch: Operation[] | undefined
|
let patch: Operation[] | undefined
|
||||||
@@ -210,7 +217,7 @@ export class AppConfigPage {
|
|||||||
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
||||||
const localPkgs = await getAllPackages(this.patch)
|
const localPkgs = await getAllPackages(this.patch)
|
||||||
const bullets = Object.keys(breakages).map(id => {
|
const bullets = Object.keys(breakages).map(id => {
|
||||||
const title = localPkgs[id].manifest.title
|
const title = getManifest(localPkgs[id]).title
|
||||||
return `<li><b>${title}</b></li>`
|
return `<li><b>${title}</b></li>`
|
||||||
})
|
})
|
||||||
message = `${message}${bullets}</ul>`
|
message = `${message}${bullets}</ul>`
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { map, take } from 'rxjs/operators'
|
|||||||
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { firstValueFrom } from 'rxjs'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'backup-select',
|
selector: 'backup-select',
|
||||||
@@ -32,13 +33,13 @@ export class BackupSelectPage {
|
|||||||
map(pkgs => {
|
map(pkgs => {
|
||||||
return Object.values(pkgs)
|
return Object.values(pkgs)
|
||||||
.map(pkg => {
|
.map(pkg => {
|
||||||
const { id, title } = pkg.manifest
|
const { id, title } = getManifest(pkg)
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
icon: pkg['static-files'].icon,
|
icon: pkg.icon,
|
||||||
disabled: pkg.state !== PackageState.Installed,
|
disabled: pkg['state-info'].state !== PackageState.Installed,
|
||||||
checked: pkg.state === PackageState.Installed,
|
checked: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
|
|||||||
@@ -8,30 +8,34 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top with-widgets">
|
<ion-content class="ion-padding-top with-widgets">
|
||||||
<ion-item-group *ngIf="pkg$ | async as pkg">
|
<ng-container *ngIf="pkg$ | async as pkg">
|
||||||
<!-- ** standard actions ** -->
|
<ion-item-group
|
||||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
*ngIf="pkg['state-info'].state === 'installed' && pkg['state-info'].manifest as manifest"
|
||||||
<app-actions-item
|
>
|
||||||
[action]="{
|
<!-- ** standard actions ** -->
|
||||||
name: 'Uninstall',
|
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||||
description: 'This will uninstall the service from StartOS and delete all data permanently.',
|
<app-actions-item
|
||||||
icon: 'trash-outline'
|
[action]="{
|
||||||
}"
|
name: 'Uninstall',
|
||||||
(click)="tryUninstall(pkg)"
|
description: 'This will uninstall the service from StartOS and delete all data permanently.',
|
||||||
></app-actions-item>
|
icon: 'trash-outline'
|
||||||
|
}"
|
||||||
|
(click)="tryUninstall(pkg)"
|
||||||
|
></app-actions-item>
|
||||||
|
|
||||||
<!-- ** specific actions ** -->
|
<!-- ** specific actions ** -->
|
||||||
<ion-item-divider *ngIf="!(pkg.manifest.actions | empty)">
|
<ion-item-divider *ngIf="!(manifest.actions | empty)">
|
||||||
Actions for {{ pkg.manifest.title }}
|
Actions for {{ manifest.title }}
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<app-actions-item
|
<app-actions-item
|
||||||
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder"
|
*ngFor="let action of manifest.actions | keyvalue: asIsOrder"
|
||||||
[action]="{
|
[action]="{
|
||||||
name: action.value.name,
|
name: action.value.name,
|
||||||
description: action.value.description,
|
description: action.value.description,
|
||||||
icon: 'play-circle-outline'
|
icon: 'play-circle-outline'
|
||||||
}"
|
}"
|
||||||
(click)="handleAction(pkg, action)"
|
(click)="handleAction(pkg.status, action)"
|
||||||
></app-actions-item>
|
></app-actions-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
</ng-container>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
DataModel,
|
DataModel,
|
||||||
|
InstalledState,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
|
StateInfo,
|
||||||
|
Status,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
|
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-actions',
|
selector: 'app-actions',
|
||||||
@@ -40,11 +44,7 @@ export class AppActionsPage {
|
|||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleAction(
|
async handleAction(status: Status, action: { key: string; value: Action }) {
|
||||||
pkg: PackageDataEntry,
|
|
||||||
action: { key: string; value: Action },
|
|
||||||
) {
|
|
||||||
const status = pkg.installed?.status
|
|
||||||
if (
|
if (
|
||||||
status &&
|
status &&
|
||||||
(action.value['allowed-statuses'] as PackageMainStatus[]).includes(
|
(action.value['allowed-statuses'] as PackageMainStatus[]).includes(
|
||||||
@@ -120,7 +120,7 @@ export class AppActionsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
|
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
|
||||||
const { title, alerts } = pkg.manifest
|
const { title, alerts } = getManifest(pkg)
|
||||||
|
|
||||||
let message =
|
let message =
|
||||||
alerts.uninstall ||
|
alerts.uninstall ||
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class AppInterfacesPage {
|
|||||||
readonly pkgId = getPkgId(this.route)
|
readonly pkgId = getPkgId(this.route)
|
||||||
|
|
||||||
readonly serviceInterfaces$ = this.patch
|
readonly serviceInterfaces$ = this.patch
|
||||||
.watch$('package-data', this.pkgId, 'installed', 'service-interfaces')
|
.watch$('package-data', this.pkgId, 'service-interfaces')
|
||||||
.pipe(
|
.pipe(
|
||||||
map(interfaces => {
|
map(interfaces => {
|
||||||
const sorted = Object.values(interfaces)
|
const sorted = Object.values(interfaces)
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
<ion-item
|
<ion-item
|
||||||
button
|
button
|
||||||
*ngIf="pkg.entry.manifest as manifest"
|
*ngIf="pkg.entry | toManifest as manifest"
|
||||||
detail="false"
|
detail="false"
|
||||||
class="service-card"
|
class="service-card"
|
||||||
[routerLink]="['/services', manifest.id]"
|
[routerLink]="['/services', manifest.id]"
|
||||||
>
|
>
|
||||||
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
|
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
|
||||||
<ion-thumbnail slot="start">
|
<ion-thumbnail slot="start">
|
||||||
<img alt="" [src]="pkg.entry['static-files'].icon" />
|
<img alt="" [src]="pkg.entry.icon" />
|
||||||
</ion-thumbnail>
|
</ion-thumbnail>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 ticker>{{ manifest.title }}</h2>
|
<h2 ticker>{{ manifest.title }}</h2>
|
||||||
<p>{{ manifest.version | displayEmver }}</p>
|
<p>{{ manifest.version | displayEmver }}</p>
|
||||||
<status
|
<status
|
||||||
[rendering]="pkg.primaryRendering"
|
[rendering]="pkg.primaryRendering"
|
||||||
[installProgress]="pkg.entry['install-progress']"
|
[installingInfo]="$any(pkg.entry['state-info'])['installing-info']"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
size="small"
|
size="small"
|
||||||
[sigtermTimeout]="sigtermTimeout"
|
[sigtermTimeout]="sigtermTimeout"
|
||||||
></status>
|
></status>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="
|
*ngIf="pkg.entry['service-interfaces'] | hasUi"
|
||||||
pkg.entry.installed && (pkg.entry.installed['service-interfaces'] | hasUi)
|
|
||||||
"
|
|
||||||
slot="end"
|
slot="end"
|
||||||
fill="clear"
|
fill="clear"
|
||||||
color="primary"
|
color="primary"
|
||||||
(click)="launchUi($event, pkg.entry.installed['service-interfaces'])"
|
(click)="launchUi($event, pkg.entry['service-interfaces'])"
|
||||||
[disabled]="!(pkg.entry.state | isLaunchable: pkgMainStatus.status)"
|
[disabled]="
|
||||||
|
!(pkg.entry['state-info'].state | isLaunchable: pkgMainStatus.status)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
InstalledPackageDataEntry,
|
|
||||||
MainStatus,
|
MainStatus,
|
||||||
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { PkgInfo } from 'src/app/util/get-package-info'
|
import { PkgInfo } from 'src/app/util/get-package-info'
|
||||||
@@ -20,7 +20,7 @@ export class AppListPkgComponent {
|
|||||||
|
|
||||||
get pkgMainStatus(): MainStatus {
|
get pkgMainStatus(): MainStatus {
|
||||||
return (
|
return (
|
||||||
this.pkg.entry.installed?.status.main || {
|
this.pkg.entry.status.main || {
|
||||||
status: PackageMainStatus.Stopped,
|
status: PackageMainStatus.Stopped,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -32,10 +32,7 @@ export class AppListPkgComponent {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUi(
|
launchUi(e: Event, interfaces: PackageDataEntry['service-interfaces']): void {
|
||||||
e: Event,
|
|
||||||
interfaces: InstalledPackageDataEntry['service-interfaces'],
|
|
||||||
): void {
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.launcherService.launch(interfaces)
|
this.launcherService.launch(interfaces)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
sizeMd="6"
|
sizeMd="6"
|
||||||
>
|
>
|
||||||
<app-list-pkg
|
<app-list-pkg
|
||||||
*ngIf="pkg.manifest.id | packageInfo | async as info"
|
*ngIf="(pkg | toManifest).id | packageInfo | async as info"
|
||||||
[pkg]="info"
|
[pkg]="info"
|
||||||
></app-list-pkg>
|
></app-list-pkg>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { filter, map, pairwise, startWith } from 'rxjs/operators'
|
import { filter, map, pairwise, startWith } from 'rxjs/operators'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
@@ -20,7 +21,7 @@ export class AppListPage {
|
|||||||
}),
|
}),
|
||||||
map(([_, pkgs]) =>
|
map(([_, pkgs]) =>
|
||||||
pkgs.sort((a, b) =>
|
pkgs.sort((a, b) =>
|
||||||
b.manifest.title.toLowerCase() > a.manifest.title.toLowerCase()
|
getManifest(b).title.toLowerCase() > getManifest(a).title.toLowerCase()
|
||||||
? -1
|
? -1
|
||||||
: 1,
|
: 1,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { Observable, combineLatest, firstValueFrom } from 'rxjs'
|
import { Observable, combineLatest } from 'rxjs'
|
||||||
import { filter, map } from 'rxjs/operators'
|
import { filter, map } from 'rxjs/operators'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { getPackageInfo, PkgInfo } from '../../../util/get-package-info'
|
import { getPackageInfo, PkgInfo } from '../../../util/get-package-info'
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class AppPropertiesPage {
|
|||||||
unmasked: { [key: string]: boolean } = {}
|
unmasked: { [key: string]: boolean } = {}
|
||||||
|
|
||||||
stopped$ = this.patch
|
stopped$ = this.patch
|
||||||
.watch$('package-data', this.pkgId, 'installed', 'status', 'main', 'status')
|
.watch$('package-data', this.pkgId, 'status', 'main', 'status')
|
||||||
.pipe(map(status => status === PackageMainStatus.Stopped))
|
.pipe(map(status => status === PackageMainStatus.Stopped))
|
||||||
|
|
||||||
@ViewChild(IonBackButtonDelegate, { static: false })
|
@ViewChild(IonBackButtonDelegate, { static: false })
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { AppShowAdditionalComponent } from './components/app-show-additional/app
|
|||||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -31,7 +31,6 @@ const routes: Routes = [
|
|||||||
declarations: [
|
declarations: [
|
||||||
AppShowPage,
|
AppShowPage,
|
||||||
HealthColorPipe,
|
HealthColorPipe,
|
||||||
ProgressDataPipe,
|
|
||||||
ToHealthChecksPipe,
|
ToHealthChecksPipe,
|
||||||
ToButtonsPipe,
|
ToButtonsPipe,
|
||||||
AppShowHeaderComponent,
|
AppShowHeaderComponent,
|
||||||
@@ -44,7 +43,7 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
StatusComponentModule,
|
InstallingProgressPipeModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
AppConfigPageModule,
|
AppConfigPageModule,
|
||||||
@@ -52,6 +51,7 @@ const routes: Routes = [
|
|||||||
LaunchablePipeModule,
|
LaunchablePipeModule,
|
||||||
UiPipeModule,
|
UiPipeModule,
|
||||||
ResponsiveColModule,
|
ResponsiveColModule,
|
||||||
|
StatusComponentModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppShowPageModule {}
|
export class AppShowPageModule {}
|
||||||
|
|||||||
@@ -7,9 +7,8 @@
|
|||||||
<!-- ** installing, updating, restoring ** -->
|
<!-- ** installing, updating, restoring ** -->
|
||||||
<ng-container *ngIf="showProgress(pkg); else installed">
|
<ng-container *ngIf="showProgress(pkg); else installed">
|
||||||
<app-show-progress
|
<app-show-progress
|
||||||
*ngIf="pkg | progressData as progressData"
|
*ngIf="pkg['state-info']['installing-info'] as installingInfo"
|
||||||
[pkg]="pkg"
|
[phases]="installingInfo.progress.phases"
|
||||||
[progressData]="progressData"
|
|
||||||
></app-show-progress>
|
></app-show-progress>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@@ -19,11 +18,13 @@
|
|||||||
<!-- ** status ** -->
|
<!-- ** status ** -->
|
||||||
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
||||||
<!-- ** installed && !backing-up ** -->
|
<!-- ** installed && !backing-up ** -->
|
||||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
<ng-container
|
||||||
|
*ngIf="isInstalled(pkg) && status.primary !== 'backing-up'"
|
||||||
|
>
|
||||||
<!-- ** health checks ** -->
|
<!-- ** health checks ** -->
|
||||||
<app-show-health-checks
|
<app-show-health-checks
|
||||||
*ngIf="isRunning(status)"
|
*ngIf="status.primary === 'running'"
|
||||||
[pkg]="pkg"
|
[manifest]="pkg['state-info'].manifest"
|
||||||
></app-show-health-checks>
|
></app-show-health-checks>
|
||||||
<!-- ** dependencies ** -->
|
<!-- ** dependencies ** -->
|
||||||
<app-show-dependencies
|
<app-show-dependencies
|
||||||
@@ -33,7 +34,9 @@
|
|||||||
<!-- ** menu ** -->
|
<!-- ** menu ** -->
|
||||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||||
<!-- ** additional ** -->
|
<!-- ** additional ** -->
|
||||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
<app-show-additional
|
||||||
|
[manifest]="pkg['state-info'].manifest"
|
||||||
|
></app-show-additional>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -3,16 +3,12 @@ import { NavController } from '@ionic/angular'
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
InstalledPackageDataEntry,
|
InstallingState,
|
||||||
Manifest,
|
Manifest,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageState,
|
UpdatingState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import {
|
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
PackageStatus,
|
|
||||||
PrimaryStatus,
|
|
||||||
renderPkgStatus,
|
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
|
||||||
import { map, tap } from 'rxjs/operators'
|
import { map, tap } from 'rxjs/operators'
|
||||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||||
import { getPkgId } from '@start9labs/shared'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
@@ -24,6 +20,13 @@ import {
|
|||||||
PkgDependencyErrors,
|
PkgDependencyErrors,
|
||||||
} from 'src/app/services/dep-error.service'
|
} from 'src/app/services/dep-error.service'
|
||||||
import { combineLatest } from 'rxjs'
|
import { combineLatest } from 'rxjs'
|
||||||
|
import {
|
||||||
|
getManifest,
|
||||||
|
isInstalled,
|
||||||
|
isInstalling,
|
||||||
|
isRestoring,
|
||||||
|
isUpdating,
|
||||||
|
} from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
export interface DependencyInfo {
|
export interface DependencyInfo {
|
||||||
id: string
|
id: string
|
||||||
@@ -35,12 +38,6 @@ export interface DependencyInfo {
|
|||||||
action: () => any
|
action: () => any
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATES = [
|
|
||||||
PackageState.Installing,
|
|
||||||
PackageState.Updating,
|
|
||||||
PackageState.Restoring,
|
|
||||||
]
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show',
|
selector: 'app-show',
|
||||||
templateUrl: './app-show.page.html',
|
templateUrl: './app-show.page.html',
|
||||||
@@ -66,6 +63,8 @@ export class AppShowPage {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
isInstalled = isInstalled
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly navCtrl: NavController,
|
private readonly navCtrl: NavController,
|
||||||
@@ -74,55 +73,44 @@ export class AppShowPage {
|
|||||||
private readonly depErrorService: DepErrorService,
|
private readonly depErrorService: DepErrorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isInstalled({ state }: PackageDataEntry): boolean {
|
showProgress(
|
||||||
return state === PackageState.Installed
|
pkg: PackageDataEntry,
|
||||||
}
|
): pkg is PackageDataEntry<InstallingState | UpdatingState> {
|
||||||
|
return isInstalling(pkg) || isUpdating(pkg) || isRestoring(pkg)
|
||||||
isRunning({ primary }: PackageStatus): boolean {
|
|
||||||
return primary === PrimaryStatus.Running
|
|
||||||
}
|
|
||||||
|
|
||||||
isBackingUp({ primary }: PackageStatus): boolean {
|
|
||||||
return primary === PrimaryStatus.BackingUp
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgress({ state }: PackageDataEntry): boolean {
|
|
||||||
return STATES.includes(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDepInfo(
|
private getDepInfo(
|
||||||
pkg: PackageDataEntry,
|
pkg: PackageDataEntry,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
): DependencyInfo[] {
|
): DependencyInfo[] {
|
||||||
const pkgInstalled = pkg.installed
|
const manifest = getManifest(pkg)
|
||||||
|
|
||||||
if (!pkgInstalled) return []
|
return Object.keys(pkg['current-dependencies'])
|
||||||
|
.filter(id => !!manifest.dependencies[id])
|
||||||
return Object.keys(pkgInstalled['current-dependencies'])
|
.map(id => this.getDepValues(pkg, manifest, id, depErrors))
|
||||||
.filter(id => !!pkgInstalled.manifest.dependencies[id])
|
|
||||||
.map(id => this.getDepValues(pkgInstalled, id, depErrors))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDepValues(
|
private getDepValues(
|
||||||
pkgInstalled: InstalledPackageDataEntry,
|
pkg: PackageDataEntry,
|
||||||
|
manifest: Manifest,
|
||||||
depId: string,
|
depId: string,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
): DependencyInfo {
|
): DependencyInfo {
|
||||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||||
pkgInstalled,
|
manifest,
|
||||||
depId,
|
depId,
|
||||||
depErrors,
|
depErrors,
|
||||||
)
|
)
|
||||||
|
|
||||||
const depInfo = pkgInstalled['dependency-info'][depId]
|
const depInfo = pkg['dependency-info'][depId]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: depId,
|
id: depId,
|
||||||
version: pkgInstalled.manifest.dependencies[depId].version, // do we want this version range?
|
version: manifest.dependencies[depId].version, // do we want this version range?
|
||||||
title: depInfo?.title || depId,
|
title: depInfo?.title || depId,
|
||||||
icon: depInfo?.icon || '',
|
icon: depInfo?.icon || '',
|
||||||
errorText: errorText
|
errorText: errorText
|
||||||
? `${errorText}. ${pkgInstalled.manifest.title} will not work as expected.`
|
? `${errorText}. ${manifest.title} will not work as expected.`
|
||||||
: '',
|
: '',
|
||||||
actionText: fixText || 'View',
|
actionText: fixText || 'View',
|
||||||
action:
|
action:
|
||||||
@@ -131,11 +119,10 @@ export class AppShowPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDepErrors(
|
private getDepErrors(
|
||||||
pkgInstalled: InstalledPackageDataEntry,
|
manifest: Manifest,
|
||||||
depId: string,
|
depId: string,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
) {
|
) {
|
||||||
const pkgManifest = pkgInstalled.manifest
|
|
||||||
const depError = depErrors[depId]
|
const depError = depErrors[depId]
|
||||||
|
|
||||||
let errorText: string | null = null
|
let errorText: string | null = null
|
||||||
@@ -146,15 +133,15 @@ export class AppShowPage {
|
|||||||
if (depError.type === DependencyErrorType.NotInstalled) {
|
if (depError.type === DependencyErrorType.NotInstalled) {
|
||||||
errorText = 'Not installed'
|
errorText = 'Not installed'
|
||||||
fixText = 'Install'
|
fixText = 'Install'
|
||||||
fixAction = () => this.fixDep(pkgManifest, 'install', depId)
|
fixAction = () => this.fixDep(manifest, 'install', depId)
|
||||||
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
||||||
errorText = 'Incorrect version'
|
errorText = 'Incorrect version'
|
||||||
fixText = 'Update'
|
fixText = 'Update'
|
||||||
fixAction = () => this.fixDep(pkgManifest, 'update', depId)
|
fixAction = () => this.fixDep(manifest, 'update', depId)
|
||||||
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||||
errorText = 'Config not satisfied'
|
errorText = 'Config not satisfied'
|
||||||
fixText = 'Auto config'
|
fixText = 'Auto config'
|
||||||
fixAction = () => this.fixDep(pkgManifest, 'configure', depId)
|
fixAction = () => this.fixDep(manifest, 'configure', depId)
|
||||||
} else if (depError.type === DependencyErrorType.NotRunning) {
|
} else if (depError.type === DependencyErrorType.NotRunning) {
|
||||||
errorText = 'Not running'
|
errorText = 'Not running'
|
||||||
fixText = 'Start'
|
fixText = 'Start'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<ion-item-divider>Additional Info</ion-item-divider>
|
<ion-item-divider>Additional Info</ion-item-divider>
|
||||||
<ion-grid *ngIf="pkg.manifest as manifest">
|
<ion-grid>
|
||||||
<ion-row>
|
<ion-row>
|
||||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ModalController, ToastController } from '@ionic/angular'
|
|||||||
import { copyToClipboard, MarkdownComponent } from '@start9labs/shared'
|
import { copyToClipboard, MarkdownComponent } from '@start9labs/shared'
|
||||||
import { from } from 'rxjs'
|
import { from } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { Manifest } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show-additional',
|
selector: 'app-show-additional',
|
||||||
@@ -12,7 +12,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
})
|
})
|
||||||
export class AppShowAdditionalComponent {
|
export class AppShowAdditionalComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg!: PackageDataEntry
|
manifest!: Manifest
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
@@ -35,10 +35,16 @@ export class AppShowAdditionalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async presentModalLicense() {
|
async presentModalLicense() {
|
||||||
|
const { id, version } = this.manifest
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: 'License',
|
title: 'License',
|
||||||
content: from(this.api.getStatic(this.pkg['static-files']['license'])),
|
content: from(
|
||||||
|
this.api.getStatic(
|
||||||
|
`/public/package-data/${id}/${version}/LICENSE.md`,
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
component: MarkdownComponent,
|
component: MarkdownComponent,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,15 +4,12 @@
|
|||||||
<ion-back-button defaultHref="services"></ion-back-button>
|
<ion-back-button defaultHref="services"></ion-back-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img class="logo" [src]="pkg['static-files'].icon" alt="" />
|
<img class="logo" [src]="pkg.icon" alt="" />
|
||||||
<ion-label>
|
<ion-label *ngIf="pkg | toManifest as manifest">
|
||||||
<h1
|
<h1 class="montserrat" [class.less-large]="manifest.title.length > 20">
|
||||||
class="montserrat"
|
{{ manifest.title }}
|
||||||
[class.less-large]="pkg.manifest.title.length > 20"
|
|
||||||
>
|
|
||||||
{{ pkg.manifest.title }}
|
|
||||||
</h1>
|
</h1>
|
||||||
<h2>{{ pkg.manifest.version | displayEmver }}</h2>
|
<h2>{{ manifest.version | displayEmver }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="pkg | toHealthChecks | async | keyvalue: asIsOrder as checks"
|
*ngIf="manifest | toHealthChecks | async | keyvalue: asIsOrder as checks"
|
||||||
>
|
>
|
||||||
<ng-container *ngIf="checks.length">
|
<ng-container *ngIf="checks.length">
|
||||||
<ion-item-divider>Health Checks</ion-item-divider>
|
<ion-item-divider>Health Checks</ion-item-divider>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 class="bold">
|
<h2 class="bold">
|
||||||
{{ pkg.manifest['health-checks'][health.key].name }}
|
{{ manifest['health-checks'][health.key].name }}
|
||||||
</h2>
|
</h2>
|
||||||
<ion-text [color]="result | healthColor">
|
<ion-text [color]="result | healthColor">
|
||||||
<p>
|
<p>
|
||||||
@@ -49,13 +49,11 @@
|
|||||||
<span
|
<span
|
||||||
*ngIf="
|
*ngIf="
|
||||||
result === HealthResult.Success &&
|
result === HealthResult.Success &&
|
||||||
pkg.manifest['health-checks'][health.key]['success-message']
|
manifest['health-checks'][health.key]['success-message']
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
:
|
:
|
||||||
{{
|
{{ manifest['health-checks'][health.key]['success-message'] }}
|
||||||
pkg.manifest['health-checks'][health.key]['success-message']
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</ion-text>
|
</ion-text>
|
||||||
@@ -70,7 +68,7 @@
|
|||||||
></ion-spinner>
|
></ion-spinner>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 class="bold">
|
<h2 class="bold">
|
||||||
{{ pkg.manifest['health-checks'][health.key].name }}
|
{{ manifest['health-checks'][health.key].name }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="primary">Awaiting result...</p>
|
<p class="primary">Awaiting result...</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
import {
|
import { HealthResult, Manifest } from 'src/app/services/patch-db/data-model'
|
||||||
HealthResult,
|
|
||||||
PackageDataEntry,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show-health-checks',
|
selector: 'app-show-health-checks',
|
||||||
@@ -13,7 +10,7 @@ import {
|
|||||||
})
|
})
|
||||||
export class AppShowHealthChecksComponent {
|
export class AppShowHealthChecksComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg!: PackageDataEntry
|
manifest!: Manifest
|
||||||
|
|
||||||
HealthResult = HealthResult
|
HealthResult = HealthResult
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<p>Downloading: {{ progressData.downloadProgress }}%</p>
|
<ng-container *ngFor="let phase of phases">
|
||||||
<ion-progress-bar
|
<p>
|
||||||
[color]="getColor('download-complete')"
|
{{ phase.name }}
|
||||||
[value]="progressData.downloadProgress / 100"
|
<span *ngIf="phase.progress | installingProgress as progress">
|
||||||
[buffer]="!progressData.downloadProgress ? 0 : 1"
|
: {{ progress * 100 }}%
|
||||||
></ion-progress-bar>
|
</span>
|
||||||
|
</p>
|
||||||
<p>Validating: {{ progressData.validateProgress }}%</p>
|
<ion-progress-bar
|
||||||
<ion-progress-bar
|
[type]="
|
||||||
[color]="getColor('validation-complete')"
|
phase.progress === true ||
|
||||||
[value]="progressData.validateProgress / 100"
|
(phase.progress !== false && phase.progress.total)
|
||||||
[buffer]="validationBuffer"
|
? 'determinate'
|
||||||
></ion-progress-bar>
|
: 'indeterminate'
|
||||||
|
"
|
||||||
<p>Unpacking: {{ progressData.unpackProgress }}%</p>
|
[color]="phase.progress === true ? 'success' : 'secondary'"
|
||||||
<ion-progress-bar
|
[value]="phase.progress | installingProgress"
|
||||||
[color]="getColor('unpack-complete')"
|
></ion-progress-bar>
|
||||||
[value]="progressData.unpackProgress / 100"
|
</ng-container>
|
||||||
[buffer]="unpackingBuffer"
|
|
||||||
></ion-progress-bar>
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import {
|
import { FullProgress } from 'src/app/services/patch-db/data-model'
|
||||||
InstallProgress,
|
|
||||||
PackageDataEntry,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
import { ProgressData } from 'src/app/types/progress-data'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show-progress',
|
selector: 'app-show-progress',
|
||||||
@@ -13,26 +9,5 @@ import { ProgressData } from 'src/app/types/progress-data'
|
|||||||
})
|
})
|
||||||
export class AppShowProgressComponent {
|
export class AppShowProgressComponent {
|
||||||
@Input()
|
@Input()
|
||||||
pkg!: PackageDataEntry
|
phases!: FullProgress['phases']
|
||||||
|
|
||||||
@Input()
|
|
||||||
progressData!: ProgressData
|
|
||||||
|
|
||||||
get unpackingBuffer(): number {
|
|
||||||
return this.progressData.validateProgress === 100 &&
|
|
||||||
!this.progressData.unpackProgress
|
|
||||||
? 0
|
|
||||||
: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
get validationBuffer(): number {
|
|
||||||
return this.progressData.downloadProgress === 100 &&
|
|
||||||
!this.progressData.validateProgress
|
|
||||||
? 0
|
|
||||||
: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
getColor(action: keyof InstallProgress): string {
|
|
||||||
return this.pkg['install-progress']?.[action] ? 'success' : 'secondary'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
<status
|
<status
|
||||||
size="x-large"
|
size="x-large"
|
||||||
weight="600"
|
weight="600"
|
||||||
[installProgress]="pkg['install-progress']"
|
[installingInfo]="$any(pkg['state-info'])['installing-info']"
|
||||||
[rendering]="PR[status.primary]"
|
[rendering]="PR[status.primary]"
|
||||||
[sigtermTimeout]="sigtermTimeout"
|
[sigtermTimeout]="sigtermTimeout"
|
||||||
></status>
|
></status>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-container *ngIf="isInstalled && (connected$ | async)">
|
<ng-container *ngIf="isInstalled(pkg) && (connected$ | async)">
|
||||||
<ion-grid>
|
<ion-grid>
|
||||||
<ion-row style="padding-left: 12px">
|
<ion-row style="padding-left: 12px">
|
||||||
<ion-col>
|
<ion-col>
|
||||||
@@ -59,7 +59,9 @@
|
|||||||
*ngIf="pkgStatus && interfaces && (interfaces | hasUi)"
|
*ngIf="pkgStatus && interfaces && (interfaces | hasUi)"
|
||||||
class="action-button"
|
class="action-button"
|
||||||
color="primary"
|
color="primary"
|
||||||
[disabled]="!(pkg.state | isLaunchable: pkgStatus.main.status)"
|
[disabled]="
|
||||||
|
!(pkg['state-info'].state | isLaunchable: pkgStatus.main.status)
|
||||||
|
"
|
||||||
(click)="launchUi(interfaces)"
|
(click)="launchUi(interfaces)"
|
||||||
>
|
>
|
||||||
<ion-icon slot="start" name="open-outline"></ion-icon>
|
<ion-icon slot="start" name="open-outline"></ion-icon>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
PrimaryStatus,
|
PrimaryStatus,
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
} from 'src/app/services/pkg-status-rendering.service'
|
||||||
import {
|
import {
|
||||||
InstalledPackageDataEntry,
|
Manifest,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
@@ -18,6 +18,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
import { ModalService } from 'src/app/services/modal.service'
|
import { ModalService } from 'src/app/services/modal.service'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||||
import { ConnectionService } from 'src/app/services/connection.service'
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
import { isInstalled, getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show-status',
|
selector: 'app-show-status',
|
||||||
@@ -34,6 +35,8 @@ export class AppShowStatusComponent {
|
|||||||
|
|
||||||
PR = PrimaryRendering
|
PR = PrimaryRendering
|
||||||
|
|
||||||
|
isInstalled = isInstalled
|
||||||
|
|
||||||
readonly connected$ = this.connectionService.connected$
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -46,18 +49,16 @@ export class AppShowStatusComponent {
|
|||||||
private readonly connectionService: ConnectionService,
|
private readonly connectionService: ConnectionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get interfaces():
|
get interfaces(): PackageDataEntry['service-interfaces'] {
|
||||||
| InstalledPackageDataEntry['service-interfaces']
|
return this.pkg['service-interfaces']
|
||||||
| undefined {
|
|
||||||
return this.pkg.installed?.['service-interfaces']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get pkgStatus(): Status | null {
|
get pkgStatus(): Status {
|
||||||
return this.pkg.installed?.status || null
|
return this.pkg.status
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInstalled(): boolean {
|
get manifest(): Manifest {
|
||||||
return this.pkg.state === PackageState.Installed
|
return getManifest(this.pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isRunning(): boolean {
|
get isRunning(): boolean {
|
||||||
@@ -82,25 +83,25 @@ export class AppShowStatusComponent {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUi(interfaces: InstalledPackageDataEntry['service-interfaces']): void {
|
launchUi(interfaces: PackageDataEntry['service-interfaces']): void {
|
||||||
this.launcherService.launch(interfaces)
|
this.launcherService.launch(interfaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentModalConfig(): Promise<void> {
|
async presentModalConfig(): Promise<void> {
|
||||||
return this.modalService.presentModalConfig({
|
return this.modalService.presentModalConfig({
|
||||||
pkgId: this.id,
|
pkgId: this.manifest.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryStart(): Promise<void> {
|
async tryStart(): Promise<void> {
|
||||||
if (this.status.dependency === 'warning') {
|
if (this.status.dependency === 'warning') {
|
||||||
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
const depErrMsg = `${this.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||||
const proceed = await this.presentAlertStart(depErrMsg)
|
const proceed = await this.presentAlertStart(depErrMsg)
|
||||||
|
|
||||||
if (!proceed) return
|
if (!proceed) return
|
||||||
}
|
}
|
||||||
|
|
||||||
const alertMsg = this.pkg.manifest.alerts.start
|
const alertMsg = this.manifest.alerts.start
|
||||||
|
|
||||||
if (alertMsg) {
|
if (alertMsg) {
|
||||||
const proceed = await this.presentAlertStart(alertMsg)
|
const proceed = await this.presentAlertStart(alertMsg)
|
||||||
@@ -112,7 +113,7 @@ export class AppShowStatusComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryStop(): Promise<void> {
|
async tryStop(): Promise<void> {
|
||||||
const { title, alerts } = this.pkg.manifest
|
const { title, alerts } = this.manifest
|
||||||
|
|
||||||
let message = alerts.stop || ''
|
let message = alerts.stop || ''
|
||||||
if (hasCurrentDeps(this.pkg)) {
|
if (hasCurrentDeps(this.pkg)) {
|
||||||
@@ -150,7 +151,7 @@ export class AppShowStatusComponent {
|
|||||||
if (hasCurrentDeps(this.pkg)) {
|
if (hasCurrentDeps(this.pkg)) {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Warning',
|
header: 'Warning',
|
||||||
message: `Services that depend on ${this.pkg.manifest.title} may temporarily experiences issues`,
|
message: `Services that depend on ${this.manifest.title} may temporarily experiences issues`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
@@ -173,10 +174,6 @@ export class AppShowStatusComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private get id(): string {
|
|
||||||
return this.pkg.manifest.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private async start(): Promise<void> {
|
private async start(): Promise<void> {
|
||||||
const loader = await this.loadingCtrl.create({
|
const loader = await this.loadingCtrl.create({
|
||||||
message: `Starting...`,
|
message: `Starting...`,
|
||||||
@@ -184,7 +181,7 @@ export class AppShowStatusComponent {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.startPackage({ id: this.id })
|
await this.embassyApi.startPackage({ id: this.manifest.id })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -199,7 +196,7 @@ export class AppShowStatusComponent {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.stopPackage({ id: this.id })
|
await this.embassyApi.stopPackage({ id: this.manifest.id })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -214,7 +211,7 @@ export class AppShowStatusComponent {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.restartPackage({ id: this.id })
|
await this.embassyApi.restartPackage({ id: this.manifest.id })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -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'])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,15 @@ import { ModalController, NavController } from '@ionic/angular'
|
|||||||
import { MarkdownComponent } from '@start9labs/shared'
|
import { MarkdownComponent } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
|
InstalledState,
|
||||||
|
Manifest,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { ModalService } from 'src/app/services/modal.service'
|
import { ModalService } from 'src/app/services/modal.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { from, map, Observable } from 'rxjs'
|
import { from, map, Observable } from 'rxjs'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
export interface Button {
|
export interface Button {
|
||||||
title: string
|
title: string
|
||||||
@@ -33,26 +36,26 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
transform(pkg: PackageDataEntry): Button[] {
|
transform(pkg: PackageDataEntry<InstalledState>): Button[] {
|
||||||
const pkgTitle = pkg.manifest.title
|
const manifest = pkg['state-info'].manifest
|
||||||
|
|
||||||
return [
|
return [
|
||||||
// instructions
|
// instructions
|
||||||
{
|
{
|
||||||
action: () => this.presentModalInstructions(pkg),
|
action: () => this.presentModalInstructions(manifest),
|
||||||
title: 'Instructions',
|
title: 'Instructions',
|
||||||
description: `Understand how to use ${pkgTitle}`,
|
description: `Understand how to use ${manifest.title}`,
|
||||||
icon: 'list-outline',
|
icon: 'list-outline',
|
||||||
highlighted$: this.patch
|
highlighted$: this.patch
|
||||||
.watch$('ui', 'ack-instructions', pkg.manifest.id)
|
.watch$('ui', 'ack-instructions', manifest.id)
|
||||||
.pipe(map(seen => !seen)),
|
.pipe(map(seen => !seen)),
|
||||||
},
|
},
|
||||||
// config
|
// config
|
||||||
{
|
{
|
||||||
action: async () =>
|
action: async () =>
|
||||||
this.modalService.presentModalConfig({ pkgId: pkg.manifest.id }),
|
this.modalService.presentModalConfig({ pkgId: manifest.id }),
|
||||||
title: 'Config',
|
title: 'Config',
|
||||||
description: `Customize ${pkgTitle}`,
|
description: `Customize ${manifest.title}`,
|
||||||
icon: 'options-outline',
|
icon: 'options-outline',
|
||||||
},
|
},
|
||||||
// properties
|
// properties
|
||||||
@@ -71,7 +74,7 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
description: `Uninstall and other commands specific to ${pkgTitle}`,
|
description: `Uninstall and other commands specific to ${manifest.title}`,
|
||||||
icon: 'flash-outline',
|
icon: 'flash-outline',
|
||||||
},
|
},
|
||||||
// interfaces
|
// interfaces
|
||||||
@@ -97,16 +100,18 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
private async presentModalInstructions(manifest: Manifest) {
|
||||||
this.apiService
|
this.apiService
|
||||||
.setDbValue<boolean>(['ack-instructions', pkg.manifest.id], true)
|
.setDbValue<boolean>(['ack-instructions', manifest.id], true)
|
||||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: 'Instructions',
|
title: 'Instructions',
|
||||||
content: from(
|
content: from(
|
||||||
this.apiService.getStatic(pkg['static-files']['instructions']),
|
this.apiService.getStatic(
|
||||||
|
`/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
component: MarkdownComponent,
|
component: MarkdownComponent,
|
||||||
@@ -115,17 +120,22 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewInMarketplaceButton(pkg: PackageDataEntry): Button {
|
private viewInMarketplaceButton(
|
||||||
const url = pkg.installed?.['marketplace-url']
|
pkg: PackageDataEntry<InstalledState>,
|
||||||
|
): Button {
|
||||||
|
const url = pkg['marketplace-url']
|
||||||
const queryParams = url ? { url } : {}
|
const queryParams = url ? { url } : {}
|
||||||
|
|
||||||
let button: Button = {
|
let button: Button = {
|
||||||
title: 'Marketplace Listing',
|
title: 'Marketplace Listing',
|
||||||
icon: 'storefront-outline',
|
icon: 'storefront-outline',
|
||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
|
this.navCtrl.navigateForward(
|
||||||
queryParams,
|
[`marketplace/${pkg['state-info'].manifest.id}`],
|
||||||
}),
|
{
|
||||||
|
queryParams,
|
||||||
|
},
|
||||||
|
),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
description: 'View service in the marketplace',
|
description: 'View service in the marketplace',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
HealthCheckResult,
|
HealthCheckResult,
|
||||||
PackageDataEntry,
|
Manifest,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { isEmptyObject } from '@start9labs/shared'
|
import { isEmptyObject } from '@start9labs/shared'
|
||||||
@@ -17,15 +17,15 @@ export class ToHealthChecksPipe implements PipeTransform {
|
|||||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||||
|
|
||||||
transform(
|
transform(
|
||||||
pkg: PackageDataEntry,
|
manifest: Manifest,
|
||||||
): Observable<Record<string, HealthCheckResult | null>> | null {
|
): Observable<Record<string, HealthCheckResult | null>> | null {
|
||||||
const healthChecks = Object.keys(pkg.manifest['health-checks']).reduce(
|
const healthChecks = Object.keys(manifest['health-checks']).reduce(
|
||||||
(obj, key) => ({ ...obj, [key]: null }),
|
(obj, key) => ({ ...obj, [key]: null }),
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
const healthChecks$ = this.patch
|
const healthChecks$ = this.patch
|
||||||
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main')
|
.watch$('package-data', manifest.id, 'status', 'main')
|
||||||
.pipe(
|
.pipe(
|
||||||
map(main => {
|
map(main => {
|
||||||
// Question: is this ok or do we have to use Object.keys
|
// Question: is this ok or do we have to use Object.keys
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
View Installed
|
View Installed
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ng-container *ngIf="localPkg; else install">
|
<ng-container *ngIf="localPkg; else install">
|
||||||
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
<ng-container *ngIf="localPkg['state-info'].state === 'installed'">
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
|
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
|
||||||
expand="block"
|
expand="block"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { ClientStorageService } from 'src/app/services/client-storage.service'
|
|||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { firstValueFrom } from 'rxjs'
|
||||||
import { dryUpdate } from 'src/app/util/dry-update'
|
import { dryUpdate } from 'src/app/util/dry-update'
|
||||||
|
|
||||||
@@ -46,8 +46,6 @@ export class MarketplaceShowControlsComponent {
|
|||||||
|
|
||||||
readonly showDevTools$ = this.ClientStorageService.showDevTools$
|
readonly showDevTools$ = this.ClientStorageService.showDevTools$
|
||||||
|
|
||||||
readonly PackageState = PackageState
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly ClientStorageService: ClientStorageService,
|
private readonly ClientStorageService: ClientStorageService,
|
||||||
@@ -60,7 +58,7 @@ export class MarketplaceShowControlsComponent {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
get localVersion(): string {
|
get localVersion(): string {
|
||||||
return this.localPkg?.manifest.version || ''
|
return this.localPkg ? getManifest(this.localPkg).version : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryInstall() {
|
async tryInstall() {
|
||||||
@@ -72,7 +70,7 @@ export class MarketplaceShowControlsComponent {
|
|||||||
if (!this.localPkg) {
|
if (!this.localPkg) {
|
||||||
this.alertInstall(url)
|
this.alertInstall(url)
|
||||||
} else {
|
} else {
|
||||||
const originalUrl = this.localPkg.installed?.['marketplace-url']
|
const originalUrl = this.localPkg['marketplace-url']
|
||||||
|
|
||||||
if (!sameUrl(url, originalUrl)) {
|
if (!sameUrl(url, originalUrl)) {
|
||||||
const proceed = await this.presentAlertDifferentMarketplace(
|
const proceed = await this.presentAlertDifferentMarketplace(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngIf="localPkg" [ngSwitch]="localPkg.state">
|
<ng-container *ngIf="localPkg">
|
||||||
<div *ngSwitchCase="PackageState.Installed">
|
<div *ngIf="isInstalled(localPkg)">
|
||||||
<ion-text
|
<ion-text
|
||||||
*ngIf="(version | compareEmver: localVersion) !== 1"
|
*ngIf="(version | compareEmver: localVersion) !== 1"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -13,15 +13,24 @@
|
|||||||
Update Available
|
Update Available
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</div>
|
</div>
|
||||||
<div *ngSwitchCase="PackageState.Removing">
|
|
||||||
|
<div *ngIf="isRemoving(localPkg)">
|
||||||
<ion-text color="danger">
|
<ion-text color="danger">
|
||||||
Removing
|
Removing
|
||||||
<span class="loading-dots"></span>
|
<span class="loading-dots"></span>
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</div>
|
</div>
|
||||||
<div *ngSwitchDefault>
|
|
||||||
|
<div
|
||||||
|
*ngIf="
|
||||||
|
isInstalling(localPkg) || isUpdating(localPkg) || isRestoring(localPkg)
|
||||||
|
"
|
||||||
|
>
|
||||||
<ion-text
|
<ion-text
|
||||||
*ngIf="localPkg['install-progress'] | installProgressDisplay as progress"
|
*ngIf="
|
||||||
|
localPkg['state-info']['installing-info']!.progress.overall
|
||||||
|
| installingProgressString as progress
|
||||||
|
"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
Installing
|
Installing
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import {
|
import {
|
||||||
PackageDataEntry,
|
isInstalled,
|
||||||
PackageState,
|
isInstalling,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
isUpdating,
|
||||||
|
isRemoving,
|
||||||
|
isRestoring,
|
||||||
|
getManifest,
|
||||||
|
} from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplace-status',
|
selector: 'marketplace-status',
|
||||||
@@ -14,9 +19,13 @@ export class MarketplaceStatusComponent {
|
|||||||
|
|
||||||
@Input() localPkg?: PackageDataEntry
|
@Input() localPkg?: PackageDataEntry
|
||||||
|
|
||||||
PackageState = PackageState
|
isInstalled = isInstalled
|
||||||
|
isInstalling = isInstalling
|
||||||
|
isUpdating = isUpdating
|
||||||
|
isRemoving = isRemoving
|
||||||
|
isRestoring = isRestoring
|
||||||
|
|
||||||
get localVersion(): string {
|
get localVersion(): string {
|
||||||
return this.localPkg?.manifest.version || ''
|
return this.localPkg ? getManifest(this.localPkg).version : ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { EmverPipesModule } from '@start9labs/shared'
|
import { EmverPipesModule } from '@start9labs/shared'
|
||||||
|
import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
|
||||||
import { InstallProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
|
|
||||||
import { MarketplaceStatusComponent } from './marketplace-status.component'
|
import { MarketplaceStatusComponent } from './marketplace-status.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -11,7 +10,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component'
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
InstallProgressPipeModule,
|
InstallingProgressPipeModule,
|
||||||
],
|
],
|
||||||
declarations: [MarketplaceStatusComponent],
|
declarations: [MarketplaceStatusComponent],
|
||||||
exports: [MarketplaceStatusComponent],
|
exports: [MarketplaceStatusComponent],
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { NotificationsPage } from './notifications.page'
|
|||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
|
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
|
||||||
|
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -22,6 +23,7 @@ const routes: Routes = [
|
|||||||
BadgeMenuComponentModule,
|
BadgeMenuComponentModule,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
BackupReportPageModule,
|
BackupReportPageModule,
|
||||||
|
UiPipeModule,
|
||||||
],
|
],
|
||||||
declarations: [NotificationsPage],
|
declarations: [NotificationsPage],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -87,8 +87,8 @@
|
|||||||
<h2>
|
<h2>
|
||||||
<b>
|
<b>
|
||||||
<span *ngIf="not['package-id'] as pkgId">
|
<span *ngIf="not['package-id'] as pkgId">
|
||||||
<!-- @TODO remove $any when Angular gets smart enough -->
|
{{ packageData[pkgId] ? (packageData[pkgId] |
|
||||||
{{ $any(packageData[pkgId])?.manifest.title || pkgId }} -
|
toManifest).title : pkgId }} -
|
||||||
</span>
|
</span>
|
||||||
<ion-text [color]="getColor(not)">{{ not.title }}</ion-text>
|
<ion-text [color]="getColor(not)">{{ not.title }}</ion-text>
|
||||||
</b>
|
</b>
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<ng-container *ngFor="let pkg of pkgs | keyvalue">
|
<ng-container *ngFor="let pkg of pkgs | keyvalue">
|
||||||
<ion-item *ngIf="backupProgress[pkg.key] as pkgProgress">
|
<ion-item *ngIf="backupProgress[pkg.key] as pkgProgress">
|
||||||
<ion-avatar slot="start">
|
<ion-avatar slot="start">
|
||||||
<img [src]="pkg.value['static-files'].icon" />
|
<img [src]="pkg.value.icon" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label>{{ pkg.value.manifest.title }}</ion-label>
|
<ion-label>{{ (pkg.value | toManifest).title }}</ion-label>
|
||||||
<!-- complete -->
|
<!-- complete -->
|
||||||
<ion-note
|
<ion-note
|
||||||
*ngIf="pkgProgress.complete; else incomplete"
|
*ngIf="pkgProgress.complete; else incomplete"
|
||||||
@@ -35,10 +35,7 @@
|
|||||||
>
|
>
|
||||||
<!-- active -->
|
<!-- active -->
|
||||||
<ion-note
|
<ion-note
|
||||||
*ngIf="
|
*ngIf="pkgStatus === 'backing-up'; else queued"
|
||||||
pkgStatus === PackageMainStatus.BackingUp;
|
|
||||||
else queued
|
|
||||||
"
|
|
||||||
class="inline"
|
class="inline"
|
||||||
slot="end"
|
slot="end"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ export class BackingUpComponent {
|
|||||||
'backup-progress',
|
'backup-progress',
|
||||||
)
|
)
|
||||||
|
|
||||||
PackageMainStatus = PackageMainStatus
|
|
||||||
|
|
||||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +33,7 @@ export class BackingUpComponent {
|
|||||||
})
|
})
|
||||||
export class PkgMainStatusPipe implements PipeTransform {
|
export class PkgMainStatusPipe implements PipeTransform {
|
||||||
transform(pkgId: string): Observable<PackageMainStatus> {
|
transform(pkgId: string): Observable<PackageMainStatus> {
|
||||||
return this.patch.watch$(
|
return this.patch.watch$('package-data', pkgId, 'status', 'main', 'status')
|
||||||
'package-data',
|
|
||||||
pkgId,
|
|
||||||
'installed',
|
|
||||||
'status',
|
|
||||||
'main',
|
|
||||||
'status',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/ba
|
|||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module'
|
import { BackupSelectPageModule } from 'src/app/modals/backup-select/backup-select.module'
|
||||||
import { PkgMainStatusPipe } from './backing-up/backing-up.component'
|
import { PkgMainStatusPipe } from './backing-up/backing-up.component'
|
||||||
|
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,7 @@ const routes: Routes = [
|
|||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
BackupDrivesComponentModule,
|
BackupDrivesComponentModule,
|
||||||
BackupSelectPageModule,
|
BackupSelectPageModule,
|
||||||
|
UiPipeModule,
|
||||||
],
|
],
|
||||||
declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe],
|
declarations: [ServerBackupPage, BackingUpComponent, PkgMainStatusPipe],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||||
import { RoundProgressModule } from 'angular-svg-round-progressbar'
|
import { RoundProgressModule } from 'angular-svg-round-progressbar'
|
||||||
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
||||||
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
|
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
|
||||||
import { MimeTypePipeModule } from '@start9labs/marketplace'
|
import { MimeTypePipeModule } from '@start9labs/marketplace'
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const routes: Routes = [
|
|||||||
SkeletonListComponentModule,
|
SkeletonListComponentModule,
|
||||||
MarkdownPipeModule,
|
MarkdownPipeModule,
|
||||||
RoundProgressModule,
|
RoundProgressModule,
|
||||||
InstallProgressPipeModule,
|
InstallingProgressPipeModule,
|
||||||
StoreIconComponentModule,
|
StoreIconComponentModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
MimeTypePipeModule,
|
MimeTypePipeModule,
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
<h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1>
|
<h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1>
|
||||||
<h2 class="inline">
|
<h2 class="inline">
|
||||||
<span>
|
<span>
|
||||||
{{ local.installed?.manifest?.version || '' |
|
{{ local['state-info'].manifest.version | displayEmver
|
||||||
displayEmver }}
|
}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ion-icon name="arrow-forward"></ion-icon>
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
<div slot="end" style="margin-left: 4px">
|
<div slot="end" style="margin-left: 4px">
|
||||||
<round-progress
|
<round-progress
|
||||||
*ngIf="local.state === 'updating' else notUpdating"
|
*ngIf="local['state-info'].state === 'updating' else notUpdating"
|
||||||
[current]="local['install-progress'] | installProgress"
|
[current]="(local['state-info']['installing-info'].progress.overall | installingProgress) || 0"
|
||||||
[max]="100"
|
[max]="100"
|
||||||
[radius]="13"
|
[radius]="13"
|
||||||
[stroke]="3"
|
[stroke]="3"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { Component, Inject } from '@angular/core'
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
|
InstalledState,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
|
UpdatingState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import {
|
import {
|
||||||
@@ -14,16 +16,20 @@ import {
|
|||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { Emver, isEmptyObject } from '@start9labs/shared'
|
import { Emver, isEmptyObject } from '@start9labs/shared'
|
||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { combineLatest, Observable } from 'rxjs'
|
import { combineLatest, map, Observable } from 'rxjs'
|
||||||
import { AlertController, NavController } from '@ionic/angular'
|
import { AlertController, NavController } from '@ionic/angular'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
import {
|
||||||
|
getAllPackages,
|
||||||
|
isInstalled,
|
||||||
|
isUpdating,
|
||||||
|
} from 'src/app/util/get-package-data'
|
||||||
import { dryUpdate } from 'src/app/util/dry-update'
|
import { dryUpdate } from 'src/app/util/dry-update'
|
||||||
|
|
||||||
interface UpdatesData {
|
interface UpdatesData {
|
||||||
hosts: StoreIdentity[]
|
hosts: StoreIdentity[]
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
localPkgs: Record<string, PackageDataEntry>
|
localPkgs: Record<string, PackageDataEntry<InstalledState | UpdatingState>>
|
||||||
errors: string[]
|
errors: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +42,14 @@ export class UpdatesPage {
|
|||||||
readonly data$: Observable<UpdatesData> = combineLatest({
|
readonly data$: Observable<UpdatesData> = combineLatest({
|
||||||
hosts: this.marketplaceService.getKnownHosts$(true),
|
hosts: this.marketplaceService.getKnownHosts$(true),
|
||||||
marketplace: this.marketplaceService.getMarketplace$(),
|
marketplace: this.marketplaceService.getMarketplace$(),
|
||||||
localPkgs: this.patch.watch$('package-data'),
|
localPkgs: this.patch.watch$('package-data').pipe(
|
||||||
|
map(pkgs =>
|
||||||
|
Object.values(pkgs).reduce((acc, curr) => {
|
||||||
|
if (isInstalled(curr) || isUpdating(curr)) return { ...acc, curr }
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, PackageDataEntry<InstalledState | UpdatingState>>),
|
||||||
|
),
|
||||||
|
),
|
||||||
errors: this.marketplaceService.getRequestErrors$(),
|
errors: this.marketplaceService.getRequestErrors$(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -154,14 +167,17 @@ export class FilterUpdatesPipe implements PipeTransform {
|
|||||||
|
|
||||||
transform(
|
transform(
|
||||||
pkgs: MarketplacePkg[],
|
pkgs: MarketplacePkg[],
|
||||||
local: Record<string, PackageDataEntry | undefined>,
|
local: Record<string, PackageDataEntry<InstalledState | UpdatingState>>,
|
||||||
): MarketplacePkg[] {
|
): MarketplacePkg[] {
|
||||||
return pkgs.filter(
|
return pkgs.filter(({ manifest }) => {
|
||||||
({ manifest }) =>
|
const localPkg = local[manifest.id]
|
||||||
|
return (
|
||||||
|
localPkg &&
|
||||||
this.emver.compare(
|
this.emver.compare(
|
||||||
manifest.version,
|
manifest.version,
|
||||||
local[manifest.id]?.installed?.manifest.version || '',
|
localPkg['state-info'].manifest.version,
|
||||||
) === 1,
|
) === 1
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
|||||||
import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info'
|
import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info'
|
||||||
import { combineLatest } from 'rxjs'
|
import { combineLatest } from 'rxjs'
|
||||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'widget-health',
|
selector: 'widget-health',
|
||||||
@@ -31,7 +32,7 @@ export class HealthComponent {
|
|||||||
]).pipe(
|
]).pipe(
|
||||||
map(([data, depErrors]) => {
|
map(([data, depErrors]) => {
|
||||||
const pkgs = Object.values<PackageDataEntry>(data).map(pkg =>
|
const pkgs = Object.values<PackageDataEntry>(data).map(pkg =>
|
||||||
getPackageInfo(pkg, depErrors[pkg.manifest.id]),
|
getPackageInfo(pkg, depErrors[getManifest(pkg).id]),
|
||||||
)
|
)
|
||||||
const result = this.labels.reduce<Record<string, number>>(
|
const result = this.labels.reduce<Record<string, number>>(
|
||||||
(acc, label) => ({
|
(acc, label) => ({
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
InstallProgressDisplayPipe,
|
InstallingProgressDisplayPipe,
|
||||||
InstallProgressPipe,
|
InstallingProgressPipe,
|
||||||
} from './install-progress.pipe'
|
} from './install-progress.pipe'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [InstallProgressPipe, InstallProgressDisplayPipe],
|
declarations: [InstallingProgressPipe, InstallingProgressDisplayPipe],
|
||||||
exports: [InstallProgressPipe, InstallProgressDisplayPipe],
|
exports: [InstallingProgressPipe, InstallingProgressDisplayPipe],
|
||||||
})
|
})
|
||||||
export class InstallProgressPipeModule {}
|
export class InstallingProgressPipeModule {}
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
import { Progress } from 'src/app/services/patch-db/data-model'
|
||||||
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
|
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'installProgress',
|
name: 'installingProgressString',
|
||||||
})
|
})
|
||||||
export class InstallProgressPipe implements PipeTransform {
|
export class InstallingProgressDisplayPipe implements PipeTransform {
|
||||||
transform(installProgress?: InstallProgress): number {
|
transform(progress: Progress): string {
|
||||||
return packageLoadingProgress(installProgress)?.totalProgress || 0
|
if (progress === true) return 'finalizing'
|
||||||
|
if (progress === false || !progress.total) return 'unknown %'
|
||||||
|
const percentage = Math.round((100 * progress.done) / progress.total)
|
||||||
|
|
||||||
|
return percentage < 99 ? String(percentage) + '%' : 'finalizing'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'installProgressDisplay',
|
name: 'installingProgress',
|
||||||
})
|
})
|
||||||
export class InstallProgressDisplayPipe implements PipeTransform {
|
export class InstallingProgressPipe implements PipeTransform {
|
||||||
transform(installProgress?: InstallProgress): string {
|
transform(progress: Progress): number | null {
|
||||||
const totalProgress =
|
if (progress === true) return 1
|
||||||
packageLoadingProgress(installProgress)?.totalProgress || 0
|
if (progress === false || !progress.total) return null
|
||||||
|
return Number((progress.done / progress.total).toFixed(2))
|
||||||
return totalProgress < 99 ? totalProgress + '%' : 'finalizing'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProgress(progress: Progress): number | null {
|
||||||
|
if (progress === true) return 1
|
||||||
|
if (progress === false || !progress.total) return null
|
||||||
|
return Number((progress.done / progress.total).toFixed(2))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { UiPipe } from './ui.pipe'
|
import { ToManifestPipe, UiPipe } from './ui.pipe'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [UiPipe],
|
declarations: [UiPipe, ToManifestPipe],
|
||||||
exports: [UiPipe],
|
exports: [UiPipe, ToManifestPipe],
|
||||||
})
|
})
|
||||||
export class UiPipeModule {}
|
export class UiPipeModule {}
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { InstalledPackageDataEntry } from '../../services/patch-db/data-model'
|
import { Manifest, PackageDataEntry } from '../../services/patch-db/data-model'
|
||||||
import { hasUi } from '../../services/config.service'
|
import { hasUi } from '../../services/config.service'
|
||||||
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'hasUi',
|
name: 'hasUi',
|
||||||
})
|
})
|
||||||
export class UiPipe implements PipeTransform {
|
export class UiPipe implements PipeTransform {
|
||||||
transform(
|
transform(interfaces: PackageDataEntry['service-interfaces']): boolean {
|
||||||
interfaces: InstalledPackageDataEntry['service-interfaces'],
|
|
||||||
): boolean {
|
|
||||||
return interfaces ? hasUi(interfaces) : false
|
return interfaces ? hasUi(interfaces) : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'toManifest',
|
||||||
|
})
|
||||||
|
export class ToManifestPipe implements PipeTransform {
|
||||||
|
transform(pkg: PackageDataEntry): Manifest {
|
||||||
|
return getManifest(pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,14 @@ import {
|
|||||||
} from 'patch-db-client'
|
} from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
InstallProgress,
|
FullProgress,
|
||||||
|
InstallingState,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
ServerStatus,
|
ServerStatus,
|
||||||
|
StateInfo,
|
||||||
|
UpdatingState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { CifsBackupTarget, RR } from './api.types'
|
import { CifsBackupTarget, RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
@@ -39,14 +42,34 @@ import { AuthService } from '../auth.service'
|
|||||||
import { ConnectionService } from '../connection.service'
|
import { ConnectionService } from '../connection.service'
|
||||||
import { StoreInfo } from '@start9labs/marketplace'
|
import { StoreInfo } from '@start9labs/marketplace'
|
||||||
|
|
||||||
const PROGRESS: InstallProgress = {
|
const PROGRESS: FullProgress = {
|
||||||
size: 120,
|
overall: {
|
||||||
downloaded: 0,
|
done: 0,
|
||||||
'download-complete': false,
|
total: 120,
|
||||||
validated: 0,
|
},
|
||||||
'validation-complete': false,
|
phases: [
|
||||||
unpacked: 0,
|
{
|
||||||
'unpack-complete': false,
|
name: 'Downloading',
|
||||||
|
progress: {
|
||||||
|
done: 0,
|
||||||
|
total: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Validating',
|
||||||
|
progress: {
|
||||||
|
done: 0,
|
||||||
|
total: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Installing',
|
||||||
|
progress: {
|
||||||
|
done: 0,
|
||||||
|
total: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -656,15 +679,30 @@ export class MockApiService extends ApiService {
|
|||||||
this.updateProgress(params.id)
|
this.updateProgress(params.id)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
const patch: Operation<PackageDataEntry>[] = [
|
const manifest = Mock.LocalPkgs[params.id]['state-info'].manifest
|
||||||
|
|
||||||
|
const patch: Operation<
|
||||||
|
PackageDataEntry<InstallingState | UpdatingState>
|
||||||
|
>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/package-data/${params.id}`,
|
path: `/package-data/${params.id}`,
|
||||||
value: {
|
value: {
|
||||||
...Mock.LocalPkgs[params.id],
|
...Mock.LocalPkgs[params.id],
|
||||||
// state: PackageState.Installing,
|
'state-info': {
|
||||||
state: PackageState.Updating,
|
// if installing
|
||||||
'install-progress': { ...PROGRESS },
|
state: PackageState.Installing,
|
||||||
|
|
||||||
|
// if updating
|
||||||
|
// state: PackageState.Updating,
|
||||||
|
// manifest,
|
||||||
|
|
||||||
|
// both
|
||||||
|
'installing-info': {
|
||||||
|
'new-manifest': manifest,
|
||||||
|
progress: PROGRESS,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -720,9 +758,13 @@ export class MockApiService extends ApiService {
|
|||||||
path: `/package-data/${id}`,
|
path: `/package-data/${id}`,
|
||||||
value: {
|
value: {
|
||||||
...Mock.LocalPkgs[id],
|
...Mock.LocalPkgs[id],
|
||||||
state: PackageState.Restoring,
|
'state-info': {
|
||||||
'install-progress': { ...PROGRESS },
|
state: PackageState.Restoring,
|
||||||
installed: undefined,
|
'installing-info': {
|
||||||
|
'new-manifest': Mock.LocalPkgs[id]['state-info'].manifest!,
|
||||||
|
progress: PROGRESS,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -948,7 +990,7 @@ export class MockApiService extends ApiService {
|
|||||||
const patch = [
|
const patch = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: `/package-data/${params.id}/state`,
|
path: `/package-data/${params.id}/state-info/state`,
|
||||||
value: PackageState.Removing,
|
value: PackageState.Removing,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -977,55 +1019,100 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateProgress(id: string): Promise<void> {
|
private async updateProgress(id: string): Promise<void> {
|
||||||
const progress = { ...PROGRESS }
|
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||||
const phases = [
|
|
||||||
{ progress: 'downloaded', completion: 'download-complete' },
|
|
||||||
{ progress: 'validated', completion: 'validation-complete' },
|
|
||||||
{ progress: 'unpacked', completion: 'unpack-complete' },
|
|
||||||
] as const
|
|
||||||
|
|
||||||
for (let phase of phases) {
|
for (let [i, phase] of progress.phases.entries()) {
|
||||||
let i = progress[phase.progress]
|
if (typeof phase.progress !== 'object' || !phase.progress.total) {
|
||||||
const size = progress?.size || 0
|
await pauseFor(2000)
|
||||||
while (i < size) {
|
|
||||||
await pauseFor(250)
|
|
||||||
i = Math.min(i + 5, size)
|
|
||||||
progress[phase.progress] = i
|
|
||||||
|
|
||||||
if (i === progress.size) {
|
const patches: Operation<any>[] = [
|
||||||
progress[phase.completion] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: `/package-data/${id}/install-progress`,
|
path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress`,
|
||||||
value: { ...progress },
|
value: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
|
||||||
|
// overall
|
||||||
|
if (typeof progress.overall === 'object' && progress.overall.total) {
|
||||||
|
const step = progress.overall.total / progress.phases.length
|
||||||
|
|
||||||
|
progress.overall.done += step
|
||||||
|
|
||||||
|
patches.push({
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/package-data/${id}/state-info/installing-info/progress/overall/done`,
|
||||||
|
value: progress.overall.done,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mockRevision(patches)
|
||||||
|
} else {
|
||||||
|
const step = phase.progress.total / 4
|
||||||
|
|
||||||
|
while (phase.progress.done < phase.progress.total) {
|
||||||
|
await pauseFor(500)
|
||||||
|
|
||||||
|
phase.progress.done += step
|
||||||
|
|
||||||
|
const patches: Operation<any>[] = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress/done`,
|
||||||
|
value: phase.progress.done,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// overall
|
||||||
|
if (typeof progress.overall === 'object' && progress.overall.total) {
|
||||||
|
const step = progress.overall.total / progress.phases.length / 4
|
||||||
|
|
||||||
|
progress.overall.done += step
|
||||||
|
|
||||||
|
patches.push({
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/package-data/${id}/state-info/installing-info/progress/overall/done`,
|
||||||
|
value: progress.overall.done,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mockRevision(patches)
|
||||||
|
|
||||||
|
if (phase.progress.done === phase.progress.total) {
|
||||||
|
await pauseFor(250)
|
||||||
|
this.mockRevision([
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/package-data/${id}/state-info/installing-info/progress/phases/${i}/progress`,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
await pauseFor(1000)
|
||||||
const patch2: Operation<any>[] = [
|
this.mockRevision([
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: `/package-data/${id}/state`,
|
path: `/package-data/${id}/state-info/installing-info/progress/overall`,
|
||||||
value: PackageState.Installed,
|
value: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
await pauseFor(1000)
|
||||||
|
const patch2: Operation<StateInfo>[] = [
|
||||||
|
{
|
||||||
|
op: PatchOp.REPLACE,
|
||||||
|
path: `/package-data/${id}/state-info`,
|
||||||
|
value: {
|
||||||
|
state: PackageState.Installed,
|
||||||
|
manifest: Mock.LocalPkgs[id]['state-info'].manifest,
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
op: PatchOp.ADD,
|
]
|
||||||
path: `/package-data/${id}/installed`,
|
this.mockRevision(patch2)
|
||||||
value: { ...Mock.LocalPkgs[id].installed },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: PatchOp.REMOVE,
|
|
||||||
path: `/package-data/${id}/install-progress`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch2)
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateOSProgress() {
|
private async updateOSProgress() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'
|
|||||||
import { WorkspaceConfig } from '@start9labs/shared'
|
import { WorkspaceConfig } from '@start9labs/shared'
|
||||||
import { types } from '@start9labs/start-sdk'
|
import { types } from '@start9labs/start-sdk'
|
||||||
import {
|
import {
|
||||||
InstalledPackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
@@ -64,7 +64,7 @@ export class ConfigService {
|
|||||||
|
|
||||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||||
launchableAddress(
|
launchableAddress(
|
||||||
interfaces: InstalledPackageDataEntry['service-interfaces'],
|
interfaces: PackageDataEntry['service-interfaces'],
|
||||||
): string {
|
): string {
|
||||||
const ui = Object.values(interfaces).find(i => i.type === 'ui')
|
const ui = Object.values(interfaces).find(i => i.type === 'ui')
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasUi(
|
export function hasUi(
|
||||||
interfaces: InstalledPackageDataEntry['service-interfaces'],
|
interfaces: PackageDataEntry['service-interfaces'],
|
||||||
): boolean {
|
): boolean {
|
||||||
return Object.values(interfaces).some(iface => iface.type === 'ui')
|
return Object.values(interfaces).some(iface => iface.type === 'ui')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
HealthCheckResult,
|
|
||||||
HealthResult,
|
HealthResult,
|
||||||
InstalledPackageDataEntry,
|
InstalledState,
|
||||||
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
|
PackageState,
|
||||||
} from './patch-db/data-model'
|
} from './patch-db/data-model'
|
||||||
import * as deepEqual from 'fast-deep-equal'
|
import * as deepEqual from 'fast-deep-equal'
|
||||||
|
import { isInstalled } from '../util/get-package-data'
|
||||||
|
|
||||||
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
||||||
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
||||||
@@ -55,14 +57,14 @@ export class DepErrorService {
|
|||||||
pkgId: string,
|
pkgId: string,
|
||||||
outerErrors: AllDependencyErrors,
|
outerErrors: AllDependencyErrors,
|
||||||
): PkgDependencyErrors {
|
): PkgDependencyErrors {
|
||||||
const pkgInstalled = pkgs[pkgId].installed
|
const pkg = pkgs[pkgId]
|
||||||
|
|
||||||
if (!pkgInstalled) return {}
|
if (!isInstalled(pkg)) return {}
|
||||||
|
|
||||||
return currentDeps(pkgs, pkgId).reduce(
|
return currentDeps(pkgs, pkgId).reduce(
|
||||||
(innerErrors, depId): PkgDependencyErrors => ({
|
(innerErrors, depId): PkgDependencyErrors => ({
|
||||||
...innerErrors,
|
...innerErrors,
|
||||||
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
|
[depId]: this.getDepError(pkgs, pkg, depId, outerErrors),
|
||||||
}),
|
}),
|
||||||
{} as PkgDependencyErrors,
|
{} as PkgDependencyErrors,
|
||||||
)
|
)
|
||||||
@@ -70,21 +72,21 @@ export class DepErrorService {
|
|||||||
|
|
||||||
private getDepError(
|
private getDepError(
|
||||||
pkgs: DataModel['package-data'],
|
pkgs: DataModel['package-data'],
|
||||||
pkgInstalled: InstalledPackageDataEntry,
|
pkg: PackageDataEntry<InstalledState>,
|
||||||
depId: string,
|
depId: string,
|
||||||
outerErrors: AllDependencyErrors,
|
outerErrors: AllDependencyErrors,
|
||||||
): DependencyError | null {
|
): DependencyError | null {
|
||||||
const depInstalled = pkgs[depId]?.installed
|
const dep = pkgs[depId]
|
||||||
|
|
||||||
// not installed
|
// not installed
|
||||||
if (!depInstalled) {
|
if (!dep || dep['state-info'].state !== PackageState.Installed) {
|
||||||
return {
|
return {
|
||||||
type: DependencyErrorType.NotInstalled,
|
type: DependencyErrorType.NotInstalled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkgManifest = pkgInstalled.manifest
|
const pkgManifest = pkg['state-info'].manifest
|
||||||
const depManifest = depInstalled.manifest
|
const depManifest = dep['state-info'].manifest
|
||||||
|
|
||||||
// incorrect version
|
// incorrect version
|
||||||
if (
|
if (
|
||||||
@@ -102,16 +104,14 @@ export class DepErrorService {
|
|||||||
|
|
||||||
// invalid config
|
// invalid config
|
||||||
if (
|
if (
|
||||||
Object.values(pkgInstalled.status['dependency-config-errors']).some(
|
Object.values(pkg.status['dependency-config-errors']).some(err => !!err)
|
||||||
err => !!err,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
type: DependencyErrorType.ConfigUnsatisfied,
|
type: DependencyErrorType.ConfigUnsatisfied,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const depStatus = depInstalled.status.main.status
|
const depStatus = dep.status.main.status
|
||||||
|
|
||||||
// not running
|
// not running
|
||||||
if (
|
if (
|
||||||
@@ -125,12 +125,8 @@ export class DepErrorService {
|
|||||||
|
|
||||||
// health check failure
|
// health check failure
|
||||||
if (depStatus === PackageMainStatus.Running) {
|
if (depStatus === PackageMainStatus.Running) {
|
||||||
for (let id of pkgInstalled['current-dependencies'][depId][
|
for (let id of pkg['current-dependencies'][depId]['health-checks']) {
|
||||||
'health-checks'
|
if (dep.status.main.health[id]?.result !== HealthResult.Success) {
|
||||||
]) {
|
|
||||||
if (
|
|
||||||
depInstalled.status.main.health[id]?.result !== HealthResult.Success
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
type: DependencyErrorType.HealthChecksFailed,
|
type: DependencyErrorType.HealthChecksFailed,
|
||||||
}
|
}
|
||||||
@@ -154,9 +150,9 @@ export class DepErrorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
|
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
|
||||||
return Object.keys(
|
return Object.keys(pkgs[id]?.['current-dependencies'] || {}).filter(
|
||||||
pkgs[id]?.installed?.['current-dependencies'] || {},
|
depId => depId !== id,
|
||||||
).filter(depId => depId !== id)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function dependencyDepth(
|
function dependencyDepth(
|
||||||
|
|||||||
@@ -107,31 +107,11 @@ export enum ServerStatus {
|
|||||||
BackingUp = 'backing-up',
|
BackingUp = 'backing-up',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PackageDataEntry {
|
export type PackageDataEntry<T extends StateInfo = StateInfo> = {
|
||||||
state: PackageState
|
'state-info': T
|
||||||
'static-files': {
|
icon: Url
|
||||||
license: Url
|
|
||||||
instructions: Url
|
|
||||||
icon: Url
|
|
||||||
}
|
|
||||||
manifest: Manifest
|
|
||||||
installed?: InstalledPackageDataEntry // exists when: installed, updating
|
|
||||||
'install-progress'?: InstallProgress // exists when: installing, updating
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PackageState {
|
|
||||||
Installing = 'installing',
|
|
||||||
Installed = 'installed',
|
|
||||||
Updating = 'updating',
|
|
||||||
Removing = 'removing',
|
|
||||||
Restoring = 'restoring',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InstalledPackageDataEntry {
|
|
||||||
status: Status
|
status: Status
|
||||||
manifest: Manifest
|
|
||||||
'last-backup': string | null
|
'last-backup': string | null
|
||||||
'system-pointers': any[]
|
|
||||||
'current-dependents': { [id: string]: CurrentDependencyInfo }
|
'current-dependents': { [id: string]: CurrentDependencyInfo }
|
||||||
'current-dependencies': { [id: string]: CurrentDependencyInfo }
|
'current-dependencies': { [id: string]: CurrentDependencyInfo }
|
||||||
'dependency-info': {
|
'dependency-info': {
|
||||||
@@ -145,6 +125,32 @@ export interface InstalledPackageDataEntry {
|
|||||||
'developer-key': string
|
'developer-key': string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StateInfo = InstalledState | InstallingState | UpdatingState
|
||||||
|
|
||||||
|
export type InstalledState = {
|
||||||
|
state: PackageState.Installed | PackageState.Removing
|
||||||
|
manifest: Manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstallingState = {
|
||||||
|
state: PackageState.Installing | PackageState.Restoring
|
||||||
|
'installing-info': InstallingInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdatingState = {
|
||||||
|
state: PackageState.Updating
|
||||||
|
'installing-info': InstallingInfo
|
||||||
|
manifest: Manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PackageState {
|
||||||
|
Installing = 'installing',
|
||||||
|
Installed = 'installed',
|
||||||
|
Updating = 'updating',
|
||||||
|
Removing = 'removing',
|
||||||
|
Restoring = 'restoring',
|
||||||
|
}
|
||||||
|
|
||||||
export interface CurrentDependencyInfo {
|
export interface CurrentDependencyInfo {
|
||||||
pointers: any[]
|
pointers: any[]
|
||||||
'health-checks': string[] // array of health check IDs
|
'health-checks': string[] // array of health check IDs
|
||||||
@@ -354,12 +360,13 @@ export interface HealthCheckResultFailure {
|
|||||||
error: string
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstallProgress {
|
export type InstallingInfo = {
|
||||||
readonly size: number | null
|
progress: FullProgress
|
||||||
readonly downloaded: number
|
'new-manifest': Manifest
|
||||||
readonly 'download-complete': boolean
|
|
||||||
readonly validated: number
|
|
||||||
readonly 'validation-complete': boolean
|
|
||||||
readonly unpacked: number
|
|
||||||
readonly 'unpack-complete': boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FullProgress = {
|
||||||
|
overall: Progress
|
||||||
|
phases: { name: string; progress: Progress }[]
|
||||||
|
}
|
||||||
|
export type Progress = boolean | { done: number; total: number | null } // false means indeterminate. true means complete
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ export function renderPkgStatus(
|
|||||||
let dependency: DependencyStatus | null = null
|
let dependency: DependencyStatus | null = null
|
||||||
let health: HealthStatus | null = null
|
let health: HealthStatus | null = null
|
||||||
|
|
||||||
if (pkg.state === PackageState.Installed && pkg.installed) {
|
if (pkg['state-info'].state === PackageState.Installed) {
|
||||||
primary = getPrimaryStatus(pkg.installed.status)
|
primary = getPrimaryStatus(pkg.status)
|
||||||
dependency = getDependencyStatus(depErrors)
|
dependency = getDependencyStatus(depErrors)
|
||||||
health = getHealthStatus(
|
health = getHealthStatus(
|
||||||
pkg.installed.status,
|
pkg.status,
|
||||||
!isEmptyObject(pkg.manifest['health-checks']),
|
!isEmptyObject(pkg['state-info'].manifest['health-checks']),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
primary = pkg.state as string as PrimaryStatus
|
primary = pkg['state-info'].state as string as PrimaryStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
return { primary, dependency, health }
|
return { primary, dependency, health }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@angular/core'
|
import { Inject, Injectable } from '@angular/core'
|
||||||
import { WINDOW } from '@ng-web-apis/common'
|
import { WINDOW } from '@ng-web-apis/common'
|
||||||
import { InstalledPackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -12,7 +12,7 @@ export class UiLauncherService {
|
|||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
launch(interfaces: InstalledPackageDataEntry['service-interfaces']): void {
|
launch(interfaces: PackageDataEntry['service-interfaces']): void {
|
||||||
this.windowRef.open(
|
this.windowRef.open(
|
||||||
this.config.launchableAddress(interfaces),
|
this.config.launchableAddress(interfaces),
|
||||||
'_blank',
|
'_blank',
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface ProgressData {
|
|
||||||
totalProgress: number
|
|
||||||
downloadProgress: number
|
|
||||||
validateProgress: number
|
|
||||||
unpackProgress: number
|
|
||||||
isComplete: boolean
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Emver } from '@start9labs/shared'
|
import { Emver } from '@start9labs/shared'
|
||||||
import { DataModel } from '../services/patch-db/data-model'
|
import { DataModel } from '../services/patch-db/data-model'
|
||||||
|
import { getManifest } from './get-package-data'
|
||||||
|
|
||||||
export function dryUpdate(
|
export function dryUpdate(
|
||||||
{ id, version }: { id: string; version: string },
|
{ id, version }: { id: string; version: string },
|
||||||
@@ -9,9 +10,10 @@ export function dryUpdate(
|
|||||||
return Object.values(pkgs)
|
return Object.values(pkgs)
|
||||||
.filter(
|
.filter(
|
||||||
pkg =>
|
pkg =>
|
||||||
Object.keys(pkg.installed?.['current-dependencies'] || {}).some(
|
Object.keys(pkg['current-dependencies'] || {}).some(
|
||||||
pkgId => pkgId === id,
|
pkgId => pkgId === id,
|
||||||
) && !emver.satisfies(version, pkg.manifest.dependencies[id].version),
|
) &&
|
||||||
|
!emver.satisfies(version, getManifest(pkg).dependencies[id].version),
|
||||||
)
|
)
|
||||||
.map(pkg => pkg.manifest.title)
|
.map(pkg => getManifest(pkg).title)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
|
InstalledState,
|
||||||
|
InstallingState,
|
||||||
|
Manifest,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
|
PackageState,
|
||||||
|
UpdatingState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { firstValueFrom } from 'rxjs'
|
||||||
|
|
||||||
@@ -17,3 +22,41 @@ export async function getAllPackages(
|
|||||||
): Promise<DataModel['package-data']> {
|
): Promise<DataModel['package-data']> {
|
||||||
return firstValueFrom(patch.watch$('package-data'))
|
return firstValueFrom(patch.watch$('package-data'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getManifest(pkg: PackageDataEntry): Manifest {
|
||||||
|
if (isInstalled(pkg) || isRemoving(pkg)) return pkg['state-info'].manifest
|
||||||
|
|
||||||
|
return (pkg['state-info'] as InstallingState)['installing-info'][
|
||||||
|
'new-manifest'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInstalled(
|
||||||
|
pkg: PackageDataEntry,
|
||||||
|
): pkg is PackageDataEntry<InstalledState> {
|
||||||
|
return pkg['state-info'].state === PackageState.Installed
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRemoving(
|
||||||
|
pkg: PackageDataEntry,
|
||||||
|
): pkg is PackageDataEntry<InstalledState> {
|
||||||
|
return pkg['state-info'].state === PackageState.Removing
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInstalling(
|
||||||
|
pkg: PackageDataEntry,
|
||||||
|
): pkg is PackageDataEntry<InstallingState> {
|
||||||
|
return pkg['state-info'].state === PackageState.Installing
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRestoring(
|
||||||
|
pkg: PackageDataEntry,
|
||||||
|
): pkg is PackageDataEntry<InstallingState> {
|
||||||
|
return pkg['state-info'].state === PackageState.Restoring
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUpdating(
|
||||||
|
pkg: PackageDataEntry,
|
||||||
|
): pkg is PackageDataEntry<UpdatingState> {
|
||||||
|
return pkg['state-info'].state === PackageState.Updating
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import {
|
|||||||
renderPkgStatus,
|
renderPkgStatus,
|
||||||
StatusRendering,
|
StatusRendering,
|
||||||
} from '../services/pkg-status-rendering.service'
|
} from '../services/pkg-status-rendering.service'
|
||||||
import { ProgressData } from 'src/app/types/progress-data'
|
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { packageLoadingProgress } from './package-loading-progress'
|
|
||||||
import { PkgDependencyErrors } from '../services/dep-error.service'
|
import { PkgDependencyErrors } from '../services/dep-error.service'
|
||||||
|
|
||||||
export function getPackageInfo(
|
export function getPackageInfo(
|
||||||
@@ -23,7 +21,6 @@ export function getPackageInfo(
|
|||||||
entry,
|
entry,
|
||||||
primaryRendering,
|
primaryRendering,
|
||||||
primaryStatus: statuses.primary,
|
primaryStatus: statuses.primary,
|
||||||
installProgress: packageLoadingProgress(entry['install-progress']),
|
|
||||||
error:
|
error:
|
||||||
statuses.health === HealthStatus.Failure ||
|
statuses.health === HealthStatus.Failure ||
|
||||||
statuses.dependency === DependencyStatus.Warning,
|
statuses.dependency === DependencyStatus.Warning,
|
||||||
@@ -40,7 +37,6 @@ export interface PkgInfo {
|
|||||||
entry: PackageDataEntry
|
entry: PackageDataEntry
|
||||||
primaryRendering: StatusRendering
|
primaryRendering: StatusRendering
|
||||||
primaryStatus: PrimaryStatus
|
primaryStatus: PrimaryStatus
|
||||||
installProgress: ProgressData | null
|
|
||||||
error: boolean
|
error: boolean
|
||||||
warning: boolean
|
warning: boolean
|
||||||
transitioning: boolean
|
transitioning: boolean
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { PackageDataEntry } from '../services/patch-db/data-model'
|
import { PackageDataEntry } from '../services/patch-db/data-model'
|
||||||
|
import { getManifest } from './get-package-data'
|
||||||
|
|
||||||
export function hasCurrentDeps(pkg: PackageDataEntry): boolean {
|
export function hasCurrentDeps(pkg: PackageDataEntry): boolean {
|
||||||
return !!Object.keys(pkg.installed?.['current-dependents'] || {}).filter(
|
return !!Object.keys(pkg['current-dependents']).filter(
|
||||||
depId => depId !== pkg.manifest.id,
|
depId => depId !== getManifest(pkg).id,
|
||||||
).length
|
).length
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user