mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Refactor AppListPage
This commit is contained in:
committed by
Aiden McClelland
parent
b546eb2504
commit
ee81ca111b
17124
ui/package-lock.json
generated
17124
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,7 @@
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"node-html-parser": "^4.1.4",
|
||||
"prettier": "^2.4.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"ts-node": "^10.2.0",
|
||||
"tslint": "^6.1.3",
|
||||
|
||||
@@ -59,7 +59,7 @@ export class WizardBaker {
|
||||
action,
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
executeAction: () => this.embassyApi.installPackage({ id, 'version-spec': version ? `=${version}` : undefined }),
|
||||
executeAction: () => this.embassyApi.installPackage({ id, version: version ? `=${version}` : undefined }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
@@ -158,7 +158,7 @@ export class WizardBaker {
|
||||
action,
|
||||
verb: 'beginning downgrade for',
|
||||
title,
|
||||
executeAction: () => this.embassyApi.installPackage({ id, 'version-spec': version ? `=${version}` : undefined }),
|
||||
executeAction: () => this.embassyApi.installPackage({ id, version: version ? `=${version}` : undefined }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="welcome">
|
||||
<h2>
|
||||
Welcome to
|
||||
<ion-text color="danger" class="embassy">Embassy</ion-text>
|
||||
</h2>
|
||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||
</div>
|
||||
<ion-button
|
||||
color="dark"
|
||||
routerLink="'/marketplace'"
|
||||
routerDirection="root"
|
||||
class="marketplace"
|
||||
>
|
||||
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||
Marketplace
|
||||
</ion-button>
|
||||
@@ -0,0 +1,18 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.embassy {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
.marketplace {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-list-empty",
|
||||
templateUrl: "app-list-empty.component.html",
|
||||
styleUrls: ["app-list-empty.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListEmptyComponent {}
|
||||
@@ -0,0 +1,10 @@
|
||||
<ion-icon
|
||||
*ngIf="pkg.error; else bulb"
|
||||
class="warning-icon"
|
||||
name="warning-outline"
|
||||
size="small"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
<ng-template #bulb>
|
||||
<div class="bulb" [style.background-color]="getColor(pkg)"></div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,24 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bulb {
|
||||
position: absolute !important;
|
||||
left: 9px !important;
|
||||
top: 8px !important;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 6px 6px rgba(255, 213, 52, 0.1);
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
position: absolute !important;
|
||||
left: 6px !important;
|
||||
top: 0 !important;
|
||||
font-size: 12px;
|
||||
border-radius: 100%;
|
||||
padding: 1px;
|
||||
background-color: rgba(255, 213, 52, 0.1);
|
||||
box-shadow: 0 0 4px 4px rgba(255, 213, 52, 0.1);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
|
||||
import { PkgInfo } from "src/app/util/get-package-info";
|
||||
|
||||
@Component({
|
||||
selector: "app-list-icon",
|
||||
templateUrl: "app-list-icon.component.html",
|
||||
styleUrls: ["app-list-icon.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListIconComponent {
|
||||
@Input()
|
||||
pkg: PkgInfo;
|
||||
|
||||
@Input()
|
||||
connectionFailure = false;
|
||||
|
||||
getColor(pkg: PkgInfo): string {
|
||||
return this.connectionFailure
|
||||
? "var(--ion-color-dark)"
|
||||
: "var(--ion-color-" + pkg.primaryRendering.color + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<ion-item
|
||||
button
|
||||
detail="false"
|
||||
[routerLink]="['/services', pkg.entry.manifest.id]"
|
||||
>
|
||||
<app-list-icon
|
||||
slot="start"
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure"
|
||||
></app-list-icon>
|
||||
<ion-thumbnail slot="start" class="ion-margin-start">
|
||||
<img alt="" [src]="pkg.entry['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ pkg.entry.manifest.title }}</h2>
|
||||
<p>{{ pkg.entry.manifest.version | displayEmver }}</p>
|
||||
<status
|
||||
[disconnected]="connectionFailure"
|
||||
[rendering]="pkg.primaryRendering"
|
||||
[installProgress]="pkg.installProgress?.totalProgress"
|
||||
weight="bold"
|
||||
size="small"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
*ngIf="pkg.entry.manifest.interfaces | hasUi"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
(click)="launchUi(pkg.entry)"
|
||||
[disabled]="
|
||||
!(
|
||||
pkg.entry.state
|
||||
| isLaunchable
|
||||
: pkg.entry.installed?.status.main.status
|
||||
: pkg.entry.manifest.interfaces
|
||||
)
|
||||
"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
} from "@angular/core";
|
||||
import { PackageDataEntry } from "src/app/services/patch-db/data-model";
|
||||
import { ConfigService } from "src/app/services/config.service";
|
||||
import { PkgInfo } from "src/app/util/get-package-info";
|
||||
|
||||
@Component({
|
||||
selector: "app-list-pkg",
|
||||
templateUrl: "app-list-pkg.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListPkgComponent {
|
||||
@Input()
|
||||
pkg: PkgInfo;
|
||||
|
||||
@Input()
|
||||
connectionFailure = false;
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
|
||||
getColor(pkg: PkgInfo): string {
|
||||
return this.connectionFailure
|
||||
? "var(--ion-color-dark)"
|
||||
: "var(--ion-color-" + pkg.primaryRendering.color + ")";
|
||||
}
|
||||
|
||||
launchUi(pkg: PackageDataEntry): void {
|
||||
this.document.defaultView.open(
|
||||
this.config.launchableURL(pkg),
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<ion-item>
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="rec.icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ rec.title }}</h2>
|
||||
<p>{{ rec.version | displayEmver }}</p>
|
||||
</ion-label>
|
||||
<ion-spinner *ngIf="loading$ | async; else actions"></ion-spinner>
|
||||
<ng-template #actions>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" color="danger" (click)="deleteRecovered(rec)">
|
||||
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
|
||||
<!-- Remove -->
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="success" (click)="install$.next(rec)">
|
||||
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
|
||||
<!-- Install -->
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { AlertController } from "@ionic/angular";
|
||||
import { ApiService } from "src/app/services/api/embassy-api.service";
|
||||
import { ErrorToastService } from "src/app/services/error-toast.service";
|
||||
import { from, merge, OperatorFunction, pipe, Subject } from "rxjs";
|
||||
import { catchError, mapTo, startWith, switchMap, tap } from "rxjs/operators";
|
||||
import { RecoveredInfo } from "src/app/util/parse-data-model";
|
||||
|
||||
@Component({
|
||||
selector: "app-list-rec",
|
||||
templateUrl: "app-list-rec.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListRecComponent {
|
||||
readonly install$ = new Subject<RecoveredInfo>();
|
||||
readonly delete$ = new Subject<RecoveredInfo>();
|
||||
|
||||
@Input()
|
||||
rec: RecoveredInfo;
|
||||
|
||||
@Output()
|
||||
readonly deleted = new EventEmitter<void>();
|
||||
|
||||
readonly installing$ = this.install$.pipe(
|
||||
switchMap(({ id }) =>
|
||||
from(this.api.installPackage({ id })).pipe(
|
||||
tap(() => this.deleted.emit()),
|
||||
loading(this.errToast)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
readonly deleting$ = this.delete$.pipe(
|
||||
switchMap(({ id }) =>
|
||||
from(this.api.deleteRecoveredPackage({ id })).pipe(loading(this.errToast))
|
||||
)
|
||||
);
|
||||
|
||||
readonly loading$ = merge(this.installing$, this.deleting$);
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController
|
||||
) {}
|
||||
|
||||
async deleteRecovered(pkg: RecoveredInfo): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: "Delete Data",
|
||||
message: `This action will permanently delete all data associated with ${pkg.title}.`,
|
||||
buttons: [
|
||||
{
|
||||
text: "Cancel",
|
||||
role: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Delete",
|
||||
handler: () => {
|
||||
this.delete$.next(pkg);
|
||||
},
|
||||
cssClass: "enter-click",
|
||||
},
|
||||
],
|
||||
});
|
||||
await alert.present();
|
||||
}
|
||||
}
|
||||
|
||||
function loading(
|
||||
errToast: ErrorToastService
|
||||
): OperatorFunction<unknown, boolean> {
|
||||
return pipe(
|
||||
startWith(true),
|
||||
catchError((e) => from(errToast.present(e))),
|
||||
mapTo(false)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- header -->
|
||||
<ion-item-divider>
|
||||
{{ reordering ? "Reorder" : "Installed Services" }}
|
||||
<ion-button slot="end" fill="clear" (click)="toggle()">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
[name]="reordering ? 'checkmark' : 'swap-vertical'"
|
||||
></ion-icon>
|
||||
{{ reordering ? "Done" : "Reorder" }}
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
|
||||
<!-- reordering -->
|
||||
<ion-list *ngIf="reordering; else grid">
|
||||
<ion-reorder-group disabled="false" (ionItemReorder)="reorder($any($event))">
|
||||
<ion-reorder *ngFor="let item of pkgs">
|
||||
<ion-item *ngIf="item | packageInfo | async as pkg" class="item">
|
||||
<app-list-icon
|
||||
slot="start"
|
||||
[pkg]="pkg"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
></app-list-icon>
|
||||
<ion-thumbnail slot="start" class="ion-margin-start">
|
||||
<img alt="" [src]="pkg.entry['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ pkg.entry.manifest.title }}</h2>
|
||||
<p>{{ pkg.entry.manifest.version | displayEmver }}</p>
|
||||
<status
|
||||
[disconnected]="connectionFailure$ | async"
|
||||
[rendering]="pkg.primaryRendering"
|
||||
[installProgress]="pkg.installProgress?.totalProgress"
|
||||
weight="bold"
|
||||
size="small"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="reorder-three" color="dark"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-reorder>
|
||||
</ion-reorder-group>
|
||||
</ion-list>
|
||||
|
||||
<!-- not reordering -->
|
||||
<ng-template #grid>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeXl="6">
|
||||
<app-list-pkg
|
||||
[pkg]="pkg | packageInfo | async"
|
||||
[connectionFailure]="connectionFailure$ | async"
|
||||
></app-list-pkg>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { ItemReorderEventDetail } from "@ionic/core";
|
||||
import { PackageDataEntry } from "src/app/services/patch-db/data-model";
|
||||
import { map } from "rxjs/operators";
|
||||
import {
|
||||
ConnectionFailure,
|
||||
ConnectionService,
|
||||
} from "src/app/services/connection.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-list-reorder",
|
||||
templateUrl: "app-list-reorder.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListReorderComponent {
|
||||
@Input()
|
||||
reordering = false;
|
||||
|
||||
@Input()
|
||||
pkgs: readonly PackageDataEntry[] = [];
|
||||
|
||||
@Output()
|
||||
readonly reorderingChange = new EventEmitter<boolean>();
|
||||
|
||||
@Output()
|
||||
readonly pkgsChange = new EventEmitter<readonly PackageDataEntry[]>();
|
||||
|
||||
readonly connectionFailure$ = this.connectionService
|
||||
.watchFailure$()
|
||||
.pipe(map((failure) => failure !== ConnectionFailure.None));
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
|
||||
toggle() {
|
||||
this.reordering = !this.reordering;
|
||||
this.reorderingChange.emit(this.reordering);
|
||||
}
|
||||
|
||||
reorder({ detail }: CustomEvent<ItemReorderEventDetail>): void {
|
||||
this.pkgs = detail.complete([...this.pkgs]);
|
||||
this.pkgsChange.emit(this.pkgs);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppListPage } from './app-list.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { NgModule } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Routes, RouterModule } from "@angular/router";
|
||||
import { IonicModule } from "@ionic/angular";
|
||||
import { AppListPage } from "./app-list.page";
|
||||
import { StatusComponentModule } from "src/app/components/status/status.component.module";
|
||||
import { SharingModule } from "src/app/modules/sharing.module";
|
||||
import { BadgeMenuComponentModule } from "src/app/components/badge-menu-button/badge-menu.component.module";
|
||||
import { AppListIconComponent } from "./app-list-icon/app-list-icon.component";
|
||||
import { AppListEmptyComponent } from "./app-list-empty/app-list-empty.component";
|
||||
import { AppListPkgComponent } from "./app-list-pkg/app-list-pkg.component";
|
||||
import { AppListRecComponent } from "./app-list-rec/app-list-rec.component";
|
||||
import { AppListReorderComponent } from "./app-list-reorder/app-list-reorder.component";
|
||||
import { PackageInfoPipe } from "./package-info.pipe";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
path: "",
|
||||
component: AppListPage,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -25,6 +31,12 @@ const routes: Routes = [
|
||||
],
|
||||
declarations: [
|
||||
AppListPage,
|
||||
AppListIconComponent,
|
||||
AppListEmptyComponent,
|
||||
AppListPkgComponent,
|
||||
AppListRecComponent,
|
||||
AppListReorderComponent,
|
||||
PackageInfoPipe,
|
||||
],
|
||||
})
|
||||
export class AppListPageModule { }
|
||||
export class AppListPageModule {}
|
||||
|
||||
@@ -8,152 +8,37 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="loading" text="Connecting to Embassy"></text-spinner>
|
||||
<text-spinner
|
||||
*ngIf="loading else data"
|
||||
text="Connecting to Embassy"
|
||||
></text-spinner>
|
||||
|
||||
<!-- not loading -->
|
||||
<div *ngIf="!loading">
|
||||
<div *ngIf="empty; else list" class="ion-text-center ion-padding">
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
|
||||
<h2>Welcome to <ion-text color="danger" style="font-family: 'Montserrat';">Embassy</ion-text></h2>
|
||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||
</div>
|
||||
<ion-button color="dark" [routerLink]="['/marketplace']" routerDirection="root" style="width: 50%;">
|
||||
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||
Marketplace
|
||||
</ion-button>
|
||||
</div>
|
||||
<ng-template #data>
|
||||
<app-list-empty
|
||||
*ngIf="empty; else list"
|
||||
class="ion-text-center ion-padding"
|
||||
></app-list-empty>
|
||||
|
||||
<ng-template #list>
|
||||
<ng-container *ngIf="pkgs.length">
|
||||
<!-- header -->
|
||||
<ion-item-divider>
|
||||
{{ reordering ? 'Reorder' : 'Installed Services' }}
|
||||
<ion-button slot="end" fill="clear" (click)="toggleReorder()">
|
||||
<ng-container *ngIf="!reordering">
|
||||
<ion-icon slot="start" name="swap-vertical"></ion-icon>
|
||||
Reorder
|
||||
</ng-container>
|
||||
<ng-container *ngIf="reordering">
|
||||
<ion-icon slot="start" name="checkmark"></ion-icon>
|
||||
Done
|
||||
</ng-container>
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<!-- not reordering -->
|
||||
<ion-grid *ngIf="!reordering">
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeXl="6">
|
||||
<ion-item button detail="false" [routerLink]="['/services', pkg.entry.manifest.id]">
|
||||
<div
|
||||
*ngIf="!pkg.error"
|
||||
slot="start"
|
||||
class="bulb"
|
||||
[style.background-color]="connectionFailure ? 'var(--ion-color-dark)' : 'var(--ion-color-' + pkg.primaryRendering.color + ')'"
|
||||
></div>
|
||||
<ion-icon
|
||||
*ngIf="pkg.error"
|
||||
slot="start"
|
||||
class="warning-icon"
|
||||
name="warning-outline"
|
||||
size="small"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
<ion-thumbnail slot="start" class="ion-margin-start">
|
||||
<img [src]="pkg.entry['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ pkg.entry.manifest.title }}</h2>
|
||||
<p>{{ pkg.entry.manifest.version | displayEmver }}</p>
|
||||
<status
|
||||
[disconnected]="connectionFailure"
|
||||
[rendering]="pkg.primaryRendering"
|
||||
[installProgress]="pkg.installProgress?.totalProgress"
|
||||
weight="bold"
|
||||
size="small"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
*ngIf="pkg.entry.manifest.interfaces | hasUi"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
(click)="launchUi(pkg.entry, $event)"
|
||||
[disabled]="!(pkg.entry.state | isLaunchable : pkg.entry.installed?.status.main.status : pkg.entry.manifest.interfaces)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- reordering -->
|
||||
<ion-list>
|
||||
<ion-reorder-group *ngIf="reordering" disabled="false" (ionItemReorder)="reorder($any($event))">
|
||||
<ion-reorder *ngFor="let pkg of pkgs">
|
||||
<ion-item style="--background: var(--ion-color-medium-shade);">
|
||||
<div
|
||||
*ngIf="!pkg.error"
|
||||
slot="start"
|
||||
class="bulb"
|
||||
[style.background-color]="connectionFailure ? 'var(--ion-color-dark)' : 'var(--ion-color-' + pkg.primaryRendering.color + ')'"
|
||||
></div>
|
||||
<ion-icon
|
||||
*ngIf="pkg.error"
|
||||
slot="start"
|
||||
class="warning-icon"
|
||||
name="warning-outline"
|
||||
size="small"
|
||||
color="warning"
|
||||
></ion-icon>
|
||||
<ion-thumbnail slot="start" class="ion-margin-start">
|
||||
<img [src]="pkg.entry['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ pkg.entry.manifest.title }}</h2>
|
||||
<p>{{ pkg.entry.manifest.version | displayEmver }}</p>
|
||||
<status
|
||||
[disconnected]="connectionFailure"
|
||||
[rendering]="pkg.primaryRendering"
|
||||
[installProgress]="pkg.installProgress?.totalProgress"
|
||||
weight="bold"
|
||||
size="small"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="reorder-three" color="dark"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-reorder>
|
||||
</ion-reorder-group>
|
||||
</ion-list>
|
||||
</ng-container>
|
||||
<app-list-reorder
|
||||
*ngIf="pkgs.length"
|
||||
[(pkgs)]="pkgs"
|
||||
[reordering]="reordering"
|
||||
(reorderingChange)="onReordering($event)"
|
||||
></app-list-reorder>
|
||||
|
||||
<ng-container *ngIf="recoveredPkgs.length && !reordering">
|
||||
<ion-item-group>
|
||||
<ion-item-divider>Recovered Services</ion-item-divider>
|
||||
<ion-item *ngFor="let rec of recoveredPkgs; let i = index;">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="rec.icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ rec.title }}</h2>
|
||||
<p>{{ rec.version | displayEmver }}</p>
|
||||
</ion-label>
|
||||
<div *ngIf="!rec.installing" slot="end">
|
||||
<ion-button fill="clear" color="danger" (click)="deleteRecovered(rec, i)">
|
||||
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
|
||||
<!-- Remove -->
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="success" (click)="install(rec)">
|
||||
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
|
||||
<!-- Install -->
|
||||
</ion-button>
|
||||
</div>
|
||||
<ion-spinner *ngIf="rec.installing"></ion-spinner>
|
||||
</ion-item>
|
||||
<app-list-rec
|
||||
*ngFor="let rec of recoveredPkgs"
|
||||
[rec]="rec"
|
||||
(deleted)="deleteRecovered(rec)"
|
||||
></app-list-rec>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -2,23 +2,6 @@ ion-item-divider {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bulb {
|
||||
position: absolute !important;
|
||||
left: 9px !important;
|
||||
top: 8px !important;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 6px 6px rgba(255,213,52, 0.1);
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
position: absolute !important;
|
||||
left: 6px !important;
|
||||
top: 0 !important;
|
||||
font-size: 12px;
|
||||
border-radius: 100%;
|
||||
padding: 1px;
|
||||
background-color: rgba(255,213,52, 0.1);
|
||||
box-shadow: 0 0 4px 4px rgba(255,213,52, 0.1);
|
||||
.item {
|
||||
--background: var(--ion-color-medium-shade);
|
||||
}
|
||||
|
||||
@@ -1,246 +1,97 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ItemReorderEventDetail } from '@ionic/core'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { DataModel, PackageDataEntry, PackageState, RecoveredPackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { combineLatest, Observable, Subscription } from 'rxjs'
|
||||
import { DependencyStatus, HealthStatus, PrimaryRendering, renderPkgStatus, StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { filter, take, tap } from 'rxjs/operators'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, map, switchMapTo, take, takeUntil, tap } from 'rxjs/operators'
|
||||
import { isEmptyObject, exists } from 'src/app/util/misc.util'
|
||||
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { parseDataModel, RecoveredInfo } from 'src/app/util/parse-data-model'
|
||||
import { DestroyService } from 'src/app/services/destroy.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-list',
|
||||
templateUrl: './app-list.page.html',
|
||||
styleUrls: ['./app-list.page.scss'],
|
||||
providers: [DestroyService],
|
||||
})
|
||||
export class AppListPage {
|
||||
PackageState = PackageState
|
||||
|
||||
subs: Subscription[] = []
|
||||
connectionFailure: boolean
|
||||
pkgs: PkgInfo[] = []
|
||||
recoveredPkgs: RecoveredInfo[] = []
|
||||
order: string[] = []
|
||||
pkgs: readonly PackageDataEntry[] = []
|
||||
recoveredPkgs: readonly RecoveredInfo[] = []
|
||||
order: readonly string[] = []
|
||||
loading = true
|
||||
empty = false
|
||||
reordering = false
|
||||
|
||||
constructor (
|
||||
private readonly config: ConfigService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly pkgLoading: PackageLoadingService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly destroy$: DestroyService,
|
||||
) { }
|
||||
|
||||
get empty (): boolean {
|
||||
return !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.patch.watch$()
|
||||
.pipe(
|
||||
filter(data => exists(data) && !isEmptyObject(data)),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(data => {
|
||||
this.loading = false
|
||||
const pkgs = JSON.parse(JSON.stringify(data['package-data'])) as { [id: string]: PackageDataEntry }
|
||||
this.recoveredPkgs = Object.entries(data['recovered-packages'])
|
||||
.filter(([id, _]) => !pkgs[id])
|
||||
.map(([id, val]) => {
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
installing: false,
|
||||
}
|
||||
})
|
||||
this.order = [...data.ui['pkg-order'] || []]
|
||||
this.patch
|
||||
.watch$()
|
||||
.pipe(
|
||||
filter((data) => exists(data) && !isEmptyObject(data)),
|
||||
take(1),
|
||||
map(parseDataModel),
|
||||
tap(({ order, pkgs, recoveredPkgs }) => {
|
||||
this.pkgs = pkgs
|
||||
this.recoveredPkgs = recoveredPkgs
|
||||
this.order = order
|
||||
this.loading = false
|
||||
|
||||
// add known pkgs in preferential order
|
||||
this.order.forEach(id => {
|
||||
if (pkgs[id]) {
|
||||
this.pkgs.push(this.subscribeToPkg(pkgs[id]))
|
||||
delete pkgs[id]
|
||||
}
|
||||
})
|
||||
|
||||
// unshift unknown packages and set order in UI DB
|
||||
if (!isEmptyObject(pkgs)) {
|
||||
Object.values(pkgs).forEach(pkg => {
|
||||
this.pkgs.unshift(this.subscribeToPkg(pkg))
|
||||
this.order.unshift(pkg.manifest.id)
|
||||
})
|
||||
this.setOrder()
|
||||
}
|
||||
|
||||
if (!this.pkgs.length && isEmptyObject(this.recoveredPkgs)) {
|
||||
this.empty = true
|
||||
}
|
||||
|
||||
this.subs.push(this.watchNewlyRecovered())
|
||||
})
|
||||
|
||||
this.subs.push(
|
||||
this.connectionService.watchFailure$()
|
||||
.subscribe(connectionFailure => {
|
||||
this.connectionFailure = connectionFailure !== ConnectionFailure.None
|
||||
}),
|
||||
)
|
||||
// set order in UI DB if there were unknown packages
|
||||
if (order.length < pkgs.length) {
|
||||
this.setOrder()
|
||||
}
|
||||
}),
|
||||
switchMapTo(this.watchNewlyRecovered()),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
launchUi (pkg: PackageDataEntry, event: Event): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
window.open(this.config.launchableURL(pkg), '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
toggleReorder (): void {
|
||||
if (this.reordering) {
|
||||
this.order = this.pkgs.map(pkg => pkg.entry.manifest.id)
|
||||
onReordering (reordering: boolean): void {
|
||||
if (!reordering) {
|
||||
this.setOrder()
|
||||
}
|
||||
this.reordering = !this.reordering
|
||||
|
||||
this.reordering = reordering
|
||||
}
|
||||
|
||||
async reorder (ev: CustomEvent<ItemReorderEventDetail>): Promise<void> {
|
||||
this.pkgs = ev.detail.complete(this.pkgs)
|
||||
deleteRecovered (rec: RecoveredInfo): void {
|
||||
this.recoveredPkgs = this.recoveredPkgs.filter((item) => item !== rec)
|
||||
}
|
||||
|
||||
async install (pkg: RecoveredInfo): Promise<void> {
|
||||
pkg.installing = true
|
||||
try {
|
||||
await this.api.installPackage({ id: pkg.id, 'version-spec': undefined })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
pkg.installing = false
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRecovered (pkg: RecoveredInfo, index: number): Promise<void> {
|
||||
|
||||
const execute = async () => {
|
||||
pkg.installing = true
|
||||
try {
|
||||
await this.api.deleteRecoveredPackage({ id: pkg.id })
|
||||
this.recoveredPkgs.splice(index, 1)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
pkg.installing = false
|
||||
}
|
||||
}
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Delete Data',
|
||||
message: `This action will permanently delete all data associated with ${pkg.title}.`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
handler: () => {
|
||||
execute()
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private watchNewlyRecovered (): Subscription {
|
||||
return combineLatest([this.watchPkgs(), this.patch.watch$('recovered-packages')])
|
||||
.subscribe(([pkgs, recoveredPkgs]) => {
|
||||
Object.keys(recoveredPkgs).forEach(id => {
|
||||
const inPkgs = !!pkgs[id]
|
||||
const recoveredIndex = this.recoveredPkgs.findIndex(rec => rec.id === id)
|
||||
if (inPkgs && recoveredIndex > -1) {
|
||||
this.recoveredPkgs.splice(recoveredIndex, 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private watchPkgs (): Observable<DataModel['package-data']> {
|
||||
return this.patch.watch$('package-data')
|
||||
.pipe(
|
||||
filter(pkgs => {
|
||||
return Object.keys(pkgs).length !== this.pkgs.length
|
||||
}),
|
||||
tap(pkgs => {
|
||||
private watchNewlyRecovered (): Observable<unknown> {
|
||||
return this.patch.watch$('package-data').pipe(
|
||||
filter((pkgs) => Object.keys(pkgs).length !== this.pkgs.length),
|
||||
tap((pkgs) => {
|
||||
const ids = Object.keys(pkgs)
|
||||
const newIds = ids.filter(
|
||||
(id) => !this.pkgs.find((pkg) => pkg.manifest.id === id),
|
||||
)
|
||||
|
||||
// remove uninstalled
|
||||
this.pkgs.forEach((pkg, i) => {
|
||||
const id = pkg.entry.manifest.id
|
||||
if (!ids.includes(id)) {
|
||||
pkg.sub.unsubscribe()
|
||||
this.pkgs.splice(i, 1)
|
||||
}
|
||||
})
|
||||
const filtered = this.pkgs.filter((pkg) =>
|
||||
ids.includes(pkg.manifest.id),
|
||||
)
|
||||
|
||||
this.empty = !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
|
||||
// add new entry to beginning of array
|
||||
const added = newIds.map((id) => pkgs[id])
|
||||
|
||||
ids.forEach(id => {
|
||||
// if already exists, return
|
||||
const pkg = this.pkgs.find(p => p.entry.manifest.id === id)
|
||||
if (pkg) return
|
||||
// otherwise add new entry to beginning of array
|
||||
this.pkgs.unshift(this.subscribeToPkg(pkgs[id]))
|
||||
})
|
||||
this.pkgs = [...added, ...filtered]
|
||||
this.recoveredPkgs.filter((rec) => !pkgs[rec.id])
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private setOrder (): void {
|
||||
this.order = this.pkgs.map((pkg) => pkg.manifest.id)
|
||||
this.api.setDbValue({ pointer: '/pkg-order', value: this.order })
|
||||
}
|
||||
|
||||
private subscribeToPkg (pkg: PackageDataEntry): PkgInfo {
|
||||
const pkgInfo: PkgInfo = {
|
||||
entry: pkg,
|
||||
primaryRendering: PrimaryRendering[renderPkgStatus(pkg).primary],
|
||||
installProgress: !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined,
|
||||
error: false,
|
||||
sub: null,
|
||||
}
|
||||
// subscribe to pkg
|
||||
pkgInfo.sub = this.patch.watch$('package-data', pkg.manifest.id).subscribe(update => {
|
||||
if (!update) return
|
||||
const statuses = renderPkgStatus(update)
|
||||
const primaryRendering = PrimaryRendering[statuses.primary]
|
||||
pkgInfo.entry = update
|
||||
pkgInfo.installProgress = !isEmptyObject(update['install-progress']) ? this.pkgLoading.transform(update['install-progress']) : undefined
|
||||
pkgInfo.primaryRendering = primaryRendering
|
||||
pkgInfo.error = statuses.health === HealthStatus.Failure || [DependencyStatus.Warning, DependencyStatus.Critical].includes(statuses.dependency)
|
||||
})
|
||||
return pkgInfo
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
interface RecoveredInfo extends RecoveredPackageDataEntry {
|
||||
id: string
|
||||
installing: boolean
|
||||
}
|
||||
|
||||
interface PkgInfo {
|
||||
entry: PackageDataEntry
|
||||
primaryRendering: StatusRendering
|
||||
installProgress: ProgressData
|
||||
error: boolean
|
||||
sub: Subscription | null
|
||||
}
|
||||
21
ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts
Normal file
21
ui/src/app/pages/apps-routes/app-list/package-info.pipe.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
import { filter, map, startWith } from "rxjs/operators";
|
||||
import { PackageDataEntry } from "../../../services/patch-db/data-model";
|
||||
import { getPackageInfo, PkgInfo } from "../../../util/get-package-info";
|
||||
import { PatchDbService } from "../../../services/patch-db/patch-db.service";
|
||||
|
||||
@Pipe({
|
||||
name: "packageInfo",
|
||||
})
|
||||
export class PackageInfoPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDbService) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
|
||||
return this.patch.watch$("package-data", pkg.manifest.id).pipe(
|
||||
filter((v) => !!v),
|
||||
map(getPackageInfo),
|
||||
startWith(getPackageInfo(pkg))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@ import { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, render
|
||||
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { MarkdownPage } from 'src/app/modals/markdown/markdown.page'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { packageLoadingProgress, ProgressData } from 'src/app/util/package-loading-progress'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show',
|
||||
@@ -60,7 +60,6 @@ export class AppShowPage {
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly config: ConfigService,
|
||||
private readonly packageLoadingService: PackageLoadingService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
) { }
|
||||
@@ -86,7 +85,7 @@ export class AppShowPage {
|
||||
|
||||
this.pkg = pkg
|
||||
this.statuses = renderPkgStatus(pkg)
|
||||
this.installProgress = !isEmptyObject(pkg['install-progress']) ? this.packageLoadingService.transform(pkg['install-progress']) : undefined
|
||||
this.installProgress = !isEmptyObject(pkg['install-progress']) ? packageLoadingProgress(pkg['install-progress']) : undefined
|
||||
}),
|
||||
|
||||
// 2
|
||||
|
||||
@@ -191,7 +191,7 @@ export class MarketplaceShowPage {
|
||||
loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.installPackage({ id, 'version-spec': version ? `=${version}` : undefined })
|
||||
await this.embassyApi.installPackage({ id, version: version ? `=${version}` : undefined })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageLoadingService, ProgressData } from '../services/package-loading.service'
|
||||
import { InstallProgress } from '../services/patch-db/data-model'
|
||||
import { packageLoadingProgress, ProgressData } from '../util/package-loading-progress';
|
||||
|
||||
@Pipe({
|
||||
name: 'installState',
|
||||
})
|
||||
export class InstallState implements PipeTransform {
|
||||
|
||||
constructor (
|
||||
private readonly pkgLoading: PackageLoadingService,
|
||||
) { }
|
||||
|
||||
transform (loadData: InstallProgress): ProgressData {
|
||||
return this.pkgLoading.transform(loadData)
|
||||
return packageLoadingProgress(loadData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export module RR {
|
||||
export type GetPackageMetricsReq = { id: string } // package.metrics
|
||||
export type GetPackageMetricsRes = Metric
|
||||
|
||||
export type InstallPackageReq = WithExpire<{ id: string, 'version-spec'?: string }> // package.install
|
||||
export type InstallPackageReq = WithExpire<{ id: string, version?: string }> // package.install
|
||||
export type InstallPackageRes = WithRevision<null>
|
||||
|
||||
export type DryUpdatePackageReq = { id: string, version: string } // package.update.dry
|
||||
|
||||
13
ui/src/app/services/destroy.service.ts
Normal file
13
ui/src/app/services/destroy.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { ReplaySubject } from "rxjs";
|
||||
|
||||
/**
|
||||
* Observable abstraction over ngOnDestroy to use with takeUntil
|
||||
*/
|
||||
@Injectable()
|
||||
export class DestroyService extends ReplaySubject<void> implements OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.next();
|
||||
this.complete();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { InstallProgress } from './patch-db/data-model'
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PackageLoadingService {
|
||||
constructor () { }
|
||||
|
||||
transform (loadData: InstallProgress): ProgressData {
|
||||
let { downloaded, validated, unpacked, size, 'download-complete': downloadComplete, 'validation-complete': validationComplete, 'unpack-complete': unpackComplete } = loadData
|
||||
// only permit 100% when "complete" == true
|
||||
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 = .2
|
||||
const unpackWeight = .7
|
||||
|
||||
const numerator = Math.floor(
|
||||
downloadWeight * downloaded +
|
||||
validateWeight * validated +
|
||||
unpackWeight * unpacked)
|
||||
|
||||
const denominator = Math.floor(size * (downloadWeight + validateWeight + unpackWeight))
|
||||
|
||||
return {
|
||||
totalProgress: Math.floor(100 * numerator / denominator),
|
||||
downloadProgress: Math.floor(100 * downloaded / size),
|
||||
validateProgress: Math.floor(100 * validated / size),
|
||||
unpackProgress: Math.floor(100 * unpacked / size),
|
||||
isComplete: downloadComplete && validationComplete && unpackComplete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProgressData {
|
||||
totalProgress: number
|
||||
downloadProgress: number
|
||||
validateProgress: number
|
||||
unpackProgress: number
|
||||
isComplete: boolean
|
||||
}
|
||||
38
ui/src/app/util/get-package-info.ts
Normal file
38
ui/src/app/util/get-package-info.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { PackageDataEntry } from '../services/patch-db/data-model'
|
||||
import {
|
||||
DependencyStatus,
|
||||
HealthStatus,
|
||||
PrimaryRendering,
|
||||
renderPkgStatus,
|
||||
StatusRendering,
|
||||
} from '../services/pkg-status-rendering.service'
|
||||
import { isEmptyObject } from './misc.util'
|
||||
import {
|
||||
packageLoadingProgress,
|
||||
ProgressData,
|
||||
} from './package-loading-progress'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
export function getPackageInfo (entry: PackageDataEntry): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry)
|
||||
|
||||
return {
|
||||
entry,
|
||||
primaryRendering: PrimaryRendering[statuses.primary],
|
||||
installProgress: !isEmptyObject(entry['install-progress'])
|
||||
? packageLoadingProgress(entry['install-progress'])
|
||||
: undefined,
|
||||
error:
|
||||
statuses.health === HealthStatus.Failure ||
|
||||
statuses.dependency === DependencyStatus.Warning ||
|
||||
statuses.dependency === DependencyStatus.Critical,
|
||||
}
|
||||
}
|
||||
|
||||
export interface PkgInfo {
|
||||
entry: PackageDataEntry
|
||||
primaryRendering: StatusRendering
|
||||
installProgress: ProgressData
|
||||
error: boolean
|
||||
sub?: Subscription | null
|
||||
}
|
||||
50
ui/src/app/util/package-loading-progress.ts
Normal file
50
ui/src/app/util/package-loading-progress.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { InstallProgress } from "src/app/services/patch-db/data-model";
|
||||
|
||||
export function packageLoadingProgress(
|
||||
loadData: InstallProgress
|
||||
): ProgressData {
|
||||
let {
|
||||
downloaded,
|
||||
validated,
|
||||
unpacked,
|
||||
size,
|
||||
"download-complete": downloadComplete,
|
||||
"validation-complete": validationComplete,
|
||||
"unpack-complete": unpackComplete,
|
||||
} = loadData;
|
||||
|
||||
// only permit 100% when "complete" == true
|
||||
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)
|
||||
);
|
||||
|
||||
return {
|
||||
totalProgress: Math.floor((100 * numerator) / denominator),
|
||||
downloadProgress: Math.floor((100 * downloaded) / size),
|
||||
validateProgress: Math.floor((100 * validated) / size),
|
||||
unpackProgress: Math.floor((100 * unpacked) / size),
|
||||
isComplete: downloadComplete && validationComplete && unpackComplete,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProgressData {
|
||||
totalProgress: number;
|
||||
downloadProgress: number;
|
||||
validateProgress: number;
|
||||
unpackProgress: number;
|
||||
isComplete: boolean;
|
||||
}
|
||||
50
ui/src/app/util/parse-data-model.ts
Normal file
50
ui/src/app/util/parse-data-model.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
RecoveredPackageDataEntry,
|
||||
} from "../services/patch-db/data-model";
|
||||
|
||||
export function parseDataModel(data: DataModel): ParsedData {
|
||||
const all = JSON.parse(JSON.stringify(data["package-data"])) as {
|
||||
[id: string]: PackageDataEntry;
|
||||
};
|
||||
|
||||
const order = [...(data.ui["pkg-order"] || [])];
|
||||
const pkgs = [];
|
||||
const recoveredPkgs = Object.entries(data["recovered-packages"])
|
||||
.filter(([id, _]) => !all[id])
|
||||
.map(([id, val]) => ({
|
||||
...val,
|
||||
id,
|
||||
}));
|
||||
|
||||
// add known packages in preferential order
|
||||
order.forEach((id) => {
|
||||
if (all[id]) {
|
||||
pkgs.push(all[id]);
|
||||
|
||||
delete all[id];
|
||||
}
|
||||
});
|
||||
|
||||
// unshift unknown packages
|
||||
Object.values(all).forEach((pkg) => {
|
||||
pkgs.unshift(pkg);
|
||||
});
|
||||
|
||||
return {
|
||||
order,
|
||||
pkgs,
|
||||
recoveredPkgs,
|
||||
};
|
||||
}
|
||||
|
||||
export interface RecoveredInfo extends RecoveredPackageDataEntry {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ParsedData {
|
||||
order: string[];
|
||||
pkgs: PackageDataEntry[];
|
||||
recoveredPkgs: RecoveredInfo[];
|
||||
}
|
||||
Reference in New Issue
Block a user