mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
finish
This commit is contained in:
committed by
Aiden McClelland
parent
36f0959bc2
commit
e864d0eb64
19090
ui/package-lock.json
generated
19090
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -111,9 +111,10 @@
|
||||
<ion-icon name="save-outline"></ion-icon>
|
||||
<ion-icon name="shield-checkmark-outline"></ion-icon>
|
||||
<ion-icon name="storefront-outline"></ion-icon>
|
||||
<ion-icon name="swap-vertical"></ion-icon>
|
||||
<ion-icon name="terminal-outline"></ion-icon>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
<ion-icon name="warning-outline"></ion-icon>
|
||||
<ion-icon name="wifi"></ion-icon>
|
||||
|
||||
<!-- Ionic components -->
|
||||
@@ -177,9 +178,9 @@
|
||||
<ion-toolbar style="border-top: 1px solid var(--ion-color-dark);" color="light">
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Install Progress: {{ (100 * (osUpdateProgress?.downloaded || 1) / (osUpdateProgress?.size || 1)).toFixed(0) }}%</ion-label>
|
||||
<ion-label>Downloading EOS: {{ (100 * (osUpdateProgress?.downloaded || 1) / (osUpdateProgress?.size || 1)).toFixed(0) }}%</ion-label>
|
||||
</ion-list-header>
|
||||
<div style="padding: 0 15px;">
|
||||
<div style="padding: 0 16px 16px 16px;">
|
||||
<ion-progress-bar
|
||||
color="secondary"
|
||||
[value]="osUpdateProgress && osUpdateProgress.downloaded / osUpdateProgress.size"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, HostListener } from '@angular/core'
|
||||
import { Component, HostListener, NgZone } from '@angular/core'
|
||||
import { Storage } from '@ionic/storage-angular'
|
||||
import { AuthService, AuthState } from './services/auth.service'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { Router, RoutesRecognized } from '@angular/router'
|
||||
import { debounceTime, distinctUntilChanged, filter, finalize, take, takeUntil, takeWhile } from 'rxjs/operators'
|
||||
import { debounceTime, distinctUntilChanged, filter, finalize, take, takeWhile } from 'rxjs/operators'
|
||||
import { AlertController, IonicSafeString, LoadingController, ToastController } from '@ionic/angular'
|
||||
import { Emver } from './services/emver.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
@@ -36,6 +36,8 @@ export class AppComponent {
|
||||
showMenu = false
|
||||
selectedIndex = 0
|
||||
offlineToast: HTMLIonToastElement
|
||||
updateToast: HTMLIonToastElement
|
||||
notificationToast: HTMLIonToastElement
|
||||
serverName: string
|
||||
unreadCount: number
|
||||
subscriptions: Subscription[] = []
|
||||
@@ -69,6 +71,7 @@ export class AppComponent {
|
||||
private readonly router: Router,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly startupAlertsService: StartupAlertsService,
|
||||
@@ -76,6 +79,7 @@ export class AppComponent {
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly zone: NgZone,
|
||||
readonly splitPane: SplitPaneTracker,
|
||||
) {
|
||||
this.init()
|
||||
@@ -106,24 +110,24 @@ export class AppComponent {
|
||||
...this.connectionService.start(),
|
||||
// watch connection to display connectivity issues
|
||||
this.watchConnection(),
|
||||
// // watch router to highlight selected menu item
|
||||
// watch router to highlight selected menu item
|
||||
this.watchRouter(),
|
||||
// // watch status to display/hide maintenance page
|
||||
// watch status to display/hide maintenance page
|
||||
])
|
||||
|
||||
this.patch.watch$()
|
||||
.pipe(
|
||||
filter(data => !isEmptyObject(data as object)),
|
||||
filter(obj => !isEmptyObject(obj)),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(_ => {
|
||||
this.subscriptions = this.subscriptions.concat([
|
||||
this.watchStatus(),
|
||||
// // watch version to refresh browser window
|
||||
// watch version to refresh browser window
|
||||
this.watchVersion(),
|
||||
// // watch unread notification count to display toast
|
||||
// watch unread notification count to display toast
|
||||
this.watchNotifications(),
|
||||
// // run startup alerts
|
||||
// run startup alerts
|
||||
this.startupAlertsService.runChecks(),
|
||||
])
|
||||
})
|
||||
@@ -135,15 +139,19 @@ export class AppComponent {
|
||||
this.patch.stop()
|
||||
this.storage.clear()
|
||||
if (this.errToast) this.errToast.dismiss()
|
||||
if (this.updateToast) this.updateToast.dismiss()
|
||||
if (this.notificationToast) this.notificationToast.dismiss()
|
||||
if (this.offlineToast) this.offlineToast.dismiss()
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
this.zone.run(() => {
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async goToWebsite (): Promise<void> {
|
||||
let url: string
|
||||
if (this.config.isTor) {
|
||||
if (this.config.isTor()) {
|
||||
url = 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'
|
||||
} else {
|
||||
url = 'https://start9.com'
|
||||
@@ -238,13 +246,16 @@ export class AppComponent {
|
||||
this.showMenu = true
|
||||
this.router.navigate([''], { replaceUrl: true })
|
||||
}
|
||||
if (ServerStatus.BackingUp === status && !route.startsWith(maintenance)) {
|
||||
if (status === ServerStatus.BackingUp && !route.startsWith(maintenance)) {
|
||||
this.showMenu = false
|
||||
this.router.navigate([maintenance], { replaceUrl: true })
|
||||
}
|
||||
if (ServerStatus.Updating === status) {
|
||||
if (status === ServerStatus.Updating) {
|
||||
this.watchUpdateProgress()
|
||||
}
|
||||
if (status === ServerStatus.Updated && !this.updateToast) {
|
||||
this.presentToastUpdated()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,13 +264,14 @@ export class AppComponent {
|
||||
.pipe(
|
||||
filter(progress => !!progress),
|
||||
takeWhile(progress => progress.downloaded < progress.size),
|
||||
// @TODO will there be a maintenance page while server is updating to new version?
|
||||
finalize(async () => {
|
||||
const maintenance = '/maintenance'
|
||||
const route = this.router.url
|
||||
if (!route.startsWith(maintenance)) {
|
||||
this.showMenu = false
|
||||
this.router.navigate([maintenance], { replaceUrl: true })
|
||||
}
|
||||
// const maintenance = '/maintenance'
|
||||
// const route = this.router.url
|
||||
// if (!route.startsWith(maintenance)) {
|
||||
// this.showMenu = false
|
||||
// this.router.navigate([maintenance], { replaceUrl: true })
|
||||
// }
|
||||
if (this.osUpdateProgress) this.osUpdateProgress.downloaded = this.osUpdateProgress.size
|
||||
await pauseFor(200)
|
||||
this.osUpdateProgress = undefined
|
||||
@@ -307,8 +319,39 @@ export class AppComponent {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async presentToastUpdated () {
|
||||
if (this.updateToast) return
|
||||
|
||||
this.updateToast = await this.toastCtrl.create({
|
||||
header: 'EOS download complete!',
|
||||
message: `Restart Embassy for changes to take effect.`,
|
||||
position: 'bottom',
|
||||
duration: 0,
|
||||
cssClass: 'success-toast',
|
||||
buttons: [
|
||||
{
|
||||
side: 'start',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
side: 'end',
|
||||
text: 'Restart',
|
||||
handler: () => {
|
||||
this.restart()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await this.updateToast.present()
|
||||
}
|
||||
|
||||
private async presentToastNotifications () {
|
||||
const toast = await this.toastCtrl.create({
|
||||
if (this.notificationToast) return
|
||||
|
||||
this.notificationToast = await this.toastCtrl.create({
|
||||
header: 'Embassy',
|
||||
message: `New notifications`,
|
||||
position: 'bottom',
|
||||
@@ -330,7 +373,7 @@ export class AppComponent {
|
||||
},
|
||||
],
|
||||
})
|
||||
await toast.present()
|
||||
await this.notificationToast.present()
|
||||
}
|
||||
|
||||
private async presentToastOffline (message: string | IonicSafeString, link?: string) {
|
||||
@@ -373,6 +416,23 @@ export class AppComponent {
|
||||
await this.offlineToast.present()
|
||||
}
|
||||
|
||||
private async restart (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Restarting...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({ })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
splitPaneVisible (e: any) {
|
||||
this.splitPane.sidebarOpen$.next(e.detail.visible)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BehaviorSubject, Subject } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'alert',
|
||||
@@ -12,5 +13,8 @@ export class AlertComponent {
|
||||
titleColor: string
|
||||
}
|
||||
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
|
||||
load () { }
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
>
|
||||
{{ disconnected ? 'Unknown' : rendering.display }}
|
||||
<ion-spinner *ngIf="rendering.showDots" class="dots dots-small" name="dots"></ion-spinner>
|
||||
<span *ngIf="installProgress">{{ installProgress }}%</span>
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { InstallProgress } from 'src/app/services/patch-db/data-model'
|
||||
import { StatusRendering } from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Component({
|
||||
@@ -12,5 +13,6 @@ export class StatusComponent {
|
||||
@Input() style?: string = 'regular'
|
||||
@Input() weight?: string = 'normal'
|
||||
@Input() disconnected?: boolean = false
|
||||
@Input() installProgress?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Installed Services</ion-title>
|
||||
<ion-title>Services</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content style="position: relative">
|
||||
<ion-content class="ion-padding">
|
||||
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="loading" text="Connecting to Embassy"></text-spinner>
|
||||
@@ -26,55 +26,139 @@
|
||||
</div>
|
||||
|
||||
<ng-template #list>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="6" sizeSm="4" sizeLg="3">
|
||||
<ion-card class="installed-card warn-shadow" [routerLink]="['/services', pkg.value.entry.manifest.id]">
|
||||
<div
|
||||
*ngIf="!pkg.value.error"
|
||||
class="bulb"
|
||||
[style.background-color]="connectionFailure ? 'var(--ion-color-dark)' : 'var(--ion-color-' + pkg.value.primaryRendering.color + ')'"
|
||||
>
|
||||
</div>
|
||||
<ion-icon *ngIf="pkg.value.error" class="warning-icon" name="warning" color="warning"></ion-icon>
|
||||
|
||||
<div class="launch-container" *ngIf="pkg.value.entry.manifest.interfaces | hasUi">
|
||||
<div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry.state | isLaunchable : pkg.value.entry.installed?.status.main.status : pkg.value.entry.manifest.interfaces)">
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
<ion-item-group *ngIf="pkgs.length">
|
||||
<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-grid *ngIf="reordering">
|
||||
<ion-row>
|
||||
<ion-col size="12" style="padding: 0;">
|
||||
<ion-reorder-group disabled="false" (ionItemReorder)="reorder($event)">
|
||||
<div *ngFor="let pkg of pkgs" style="padding: 5px;">
|
||||
<ion-reorder>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<img style="position: absolute" class="main-img" [src]="pkg.value.entry['static-files'].icon" [alt]="pkg.value.entry.manifest.title" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<ion-card-header>
|
||||
<ion-card-title>
|
||||
{{ pkg.value.entry.manifest.title }}
|
||||
</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-toolbar class="status-toolbar">
|
||||
<ion-title>
|
||||
<ion-text
|
||||
*ngIf="!!pkg.value.installProgress"
|
||||
color="primary"
|
||||
style="font-weight: bold; font-size: calc(10px + .3vw);"
|
||||
>
|
||||
{{ pkg.value.entry.state | titlecase }} {{ pkg.value.installProgress.totalProgress }}%
|
||||
</ion-text>
|
||||
<status
|
||||
*ngIf="[PackageState.Installed, PackageState.Removing] | includes : pkg.value.entry.state"
|
||||
[disconnected]="connectionFailure"
|
||||
[rendering]="pkg.value.primaryRendering"
|
||||
weight="bold"
|
||||
size="calc(10px + .5vw)"
|
||||
>
|
||||
</status>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-reorder-group>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item-group>
|
||||
|
||||
<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)="uninstall(rec, i)">
|
||||
<ion-icon slot="icon-only" name="close"></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>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
@@ -1,85 +1,24 @@
|
||||
.installed-card {
|
||||
margin: 4px;
|
||||
padding: 4px;
|
||||
background: linear-gradient(37deg, #333333, #131313);
|
||||
border-radius: 6px 0 6px 6px;
|
||||
text-align: center;
|
||||
|
||||
ion-card-header {
|
||||
padding: 0 10px;
|
||||
|
||||
ion-card-title {
|
||||
font-family: 'Montserrat';
|
||||
font-size: calc(10px + .5vw);
|
||||
margin-bottom: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-img {
|
||||
width: 40%;
|
||||
margin: 12px;
|
||||
ion-item-divider {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bulb {
|
||||
height: calc(10px + .5vw);
|
||||
width: calc(10px + .5vw);
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 4px 4px rgba(255,213,52, 0.1);
|
||||
position: absolute !important;
|
||||
left: 10px !important;
|
||||
top: 10px !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: 8px !important;
|
||||
top: 8px !important;
|
||||
font-size: 19px;
|
||||
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);
|
||||
}
|
||||
|
||||
.launch-button-triangle {
|
||||
right: 0px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 24px;
|
||||
border-color: rgb(70 193 255 / 75%) rgb(70 193 255 / 75%) transparent transparent;
|
||||
&:hover {
|
||||
border-color: rgb(70 193 255) rgb(70 193 255) transparent transparent;
|
||||
}
|
||||
ion-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.launch-disabled {
|
||||
pointer-events: none;
|
||||
border-color: transparent;
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
ion-icon {
|
||||
color: var(--ion-color-dark-shade);
|
||||
}
|
||||
}
|
||||
|
||||
.launch-container {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.status-toolbar {
|
||||
--min-height: 36px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { Component } from '@angular/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 { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { Subscription } from 'rxjs'
|
||||
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 } from 'rxjs/operators'
|
||||
import { isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { filter, take, 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'
|
||||
|
||||
@Component({
|
||||
selector: 'app-list',
|
||||
@@ -19,75 +21,74 @@ export class AppListPage {
|
||||
|
||||
subs: Subscription[] = []
|
||||
connectionFailure: boolean
|
||||
pkgs: { [id: string]: PkgInfo } = { }
|
||||
pkgs: PkgInfo[] = []
|
||||
recoveredPkgs: RecoveredInfo[] = []
|
||||
order: string[] = []
|
||||
loading = true
|
||||
empty = false
|
||||
reordering = false
|
||||
|
||||
constructor (
|
||||
private readonly config: ConfigService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly pkgLoading: PackageLoadingService,
|
||||
public readonly patch: PatchDbService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.subs = [
|
||||
this.patch.watch$('package-data')
|
||||
.pipe(
|
||||
filter(obj => {
|
||||
return obj &&
|
||||
(
|
||||
isEmptyObject(obj) ||
|
||||
Object.keys(obj).length !== Object.keys(this.pkgs).length
|
||||
)
|
||||
}),
|
||||
)
|
||||
.subscribe(pkgs => {
|
||||
this.loading = false
|
||||
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']).map(([id, val]) => {
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
installing: false,
|
||||
}
|
||||
})
|
||||
this.order = [...data.ui['pkg-order'] || []]
|
||||
|
||||
const ids = Object.keys(pkgs)
|
||||
// add known pkgs in preferential order
|
||||
this.order.forEach(id => {
|
||||
if (pkgs[id]) {
|
||||
this.pkgs.push(this.buildPkg(pkgs[id]))
|
||||
delete pkgs[id]
|
||||
}
|
||||
})
|
||||
|
||||
this.empty = !ids.length
|
||||
|
||||
Object.keys(this.pkgs).forEach(id => {
|
||||
if (!ids.includes(id)) {
|
||||
this.pkgs[id].sub.unsubscribe()
|
||||
delete this.pkgs[id]
|
||||
}
|
||||
// add unknown packages to end and set order in UI DB
|
||||
if (!isEmptyObject(pkgs)) {
|
||||
Object.values(pkgs).forEach(pkg => {
|
||||
this.pkgs.unshift(this.buildPkg(pkg))
|
||||
this.order.unshift(pkg.manifest.id)
|
||||
})
|
||||
this.setOrder()
|
||||
}
|
||||
|
||||
ids.forEach(id => {
|
||||
// if already subscribed, return
|
||||
if (this.pkgs[id]) return
|
||||
this.pkgs[id] = {
|
||||
entry: pkgs[id],
|
||||
primaryRendering: PrimaryRendering[renderPkgStatus(pkgs[id]).primary],
|
||||
installProgress: !isEmptyObject(pkgs[id]['install-progress']) ? this.pkgLoading.transform(pkgs[id]['install-progress']) : undefined,
|
||||
error: false,
|
||||
sub: null,
|
||||
}
|
||||
// subscribe to pkg
|
||||
this.pkgs[id].sub = this.patch.watch$('package-data', id).subscribe(pkg => {
|
||||
if (!pkg) return
|
||||
const statuses = renderPkgStatus(pkg)
|
||||
const primaryRendering = PrimaryRendering[statuses.primary]
|
||||
this.pkgs[id].entry = pkg
|
||||
this.pkgs[id].installProgress = !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined
|
||||
this.pkgs[id].primaryRendering = primaryRendering
|
||||
this.pkgs[id].error = statuses.health === HealthStatus.Failure || [DependencyStatus.Issue, DependencyStatus.Critical].includes(statuses.dependency)
|
||||
})
|
||||
})
|
||||
}),
|
||||
if (!this.pkgs.length && isEmptyObject(this.recoveredPkgs)) {
|
||||
this.empty = true
|
||||
}
|
||||
|
||||
this.subs.push(this.subscribeBoth())
|
||||
})
|
||||
|
||||
this.subs.push(
|
||||
this.connectionService.watchFailure$()
|
||||
.subscribe(connectionFailure => {
|
||||
this.connectionFailure = connectionFailure !== ConnectionFailure.None
|
||||
}),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
Object.values(this.pkgs).forEach(pkg => pkg.sub.unsubscribe())
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
@@ -97,15 +98,128 @@ export class AppListPage {
|
||||
window.open(this.config.launchableURL(pkg), '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
toggleReorder (): void {
|
||||
if (this.reordering) {
|
||||
const newPkgs = []
|
||||
this.order.forEach(id => {
|
||||
const pkg = this.pkgs.find(pkg => pkg.entry.manifest.id === id)
|
||||
if (pkg) {
|
||||
newPkgs.push(pkg)
|
||||
}
|
||||
})
|
||||
this.pkgs = newPkgs
|
||||
this.setOrder()
|
||||
}
|
||||
this.reordering = !this.reordering
|
||||
}
|
||||
|
||||
async reorder (ev: any): Promise<void> {
|
||||
ev.detail.complete()
|
||||
const toMove = this.order.splice(ev.detail.from, 1)[0]
|
||||
this.order.splice(ev.detail.to, 0, toMove)
|
||||
}
|
||||
|
||||
async install (pkg: RecoveredInfo): Promise<void> {
|
||||
pkg.installing = true
|
||||
try {
|
||||
await this.api.installPackage({ id: pkg.id, version: pkg.version })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
pkg.installing = false
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall (pkg: RecoveredInfo, index: number): Promise<void> {
|
||||
pkg.installing = true
|
||||
try {
|
||||
await this.api.uninstallPackage({ id: pkg.id })
|
||||
this.recoveredPkgs.splice(index, 1)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
pkg.installing = false
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeBoth (): 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(obj => {
|
||||
return Object.keys(obj).length !== this.pkgs.length
|
||||
}),
|
||||
tap(pkgs => {
|
||||
const ids = Object.keys(pkgs)
|
||||
|
||||
this.pkgs.forEach((pkg, i) => {
|
||||
const id = pkg.entry.manifest.id
|
||||
if (!ids.includes(id)) {
|
||||
pkg.sub.unsubscribe()
|
||||
this.pkgs.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
||||
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.buildPkg(pkgs[id]))
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private setOrder (): void {
|
||||
this.api.setDbValue({ pointer: '/pkg-order', value: this.order })
|
||||
}
|
||||
|
||||
private buildPkg (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(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined
|
||||
pkgInfo.primaryRendering = primaryRendering
|
||||
pkgInfo.error = statuses.health === HealthStatus.Failure || [DependencyStatus.Issue, 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
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,13 @@
|
||||
<ion-item-divider>Status</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label style="overflow: visible;">
|
||||
<status [disconnected]="connectionFailure" size="x-large" weight="500" [rendering]="PR[statuses.primary]"></status>
|
||||
<status
|
||||
[disconnected]="connectionFailure"
|
||||
size="x-large"
|
||||
weight="500"
|
||||
[installProgress]="installProgress?.totalProgress"
|
||||
[rendering]="PR[statuses.primary]"
|
||||
></status>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="pkg.state === PackageState.Installed && !connectionFailure">
|
||||
<ion-button slot="end" class="action-button" *ngIf="pkg.manifest.interfaces | hasUi" [disabled]="!(pkg.state | isLaunchable : pkg.installed.status.main.status : pkg.manifest.interfaces)" (click)="launchUi()">
|
||||
@@ -64,7 +70,7 @@
|
||||
<ng-container *ngIf="$any(health.value).result as result">
|
||||
<ion-spinner class="icon-spinner" color="primary" slot="start" *ngIf="[HealthResult.Starting, HealthResult.Loading] | includes : result"></ion-spinner>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Success" name="checkmark" color="success"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Failure" name="warning" color="warning"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Failure" name="warning-outline" color="warning"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="result === HealthResult.Disabled" name="remove" color="dark"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold;">{{ health.key }}</h2>
|
||||
@@ -86,7 +92,7 @@
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="inline" style="font-family: 'Montserrat'">
|
||||
<ion-icon *ngIf="!!dep.value.errorText" slot="start" name="warning" color="warning"></ion-icon>
|
||||
<ion-icon *ngIf="!!dep.value.errorText" slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
{{ dep.value.title }}
|
||||
</h2>
|
||||
<p>{{ dep.value.version | displayEmver }}</p>
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
*ngIf="!pkgs.length && category ==='updates'"
|
||||
style="text-align: center;"
|
||||
>
|
||||
<h1>👏👏👏 Up to date! 👏👏👏</h1>
|
||||
<h1>All services are up to date!</h1>
|
||||
</div>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<ion-item-group style="margin-bottom: 16px;">
|
||||
<ion-item-divider>
|
||||
<ion-button slot="end" fill="clear" (click)="deleteAll()">
|
||||
<ion-icon slot="start" name="trash-outline"></ion-icon>
|
||||
Delete All
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
|
||||
@@ -22,18 +22,6 @@
|
||||
|
||||
<ion-item-divider>Country</ion-item-divider>
|
||||
|
||||
<!-- loading -->
|
||||
<!-- <ng-container *ngIf="loading">
|
||||
<ion-item class="skeleton-parts">
|
||||
<ion-button slot="start" fill="clear">
|
||||
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<ion-skeleton-text animated style="width: 150px;"></ion-skeleton-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container> -->
|
||||
|
||||
<!-- not loading -->
|
||||
<ion-item button detail="false" (click)="presentAlertCountry()" [disabled]="loading">
|
||||
<ion-icon slot="start" name="earth-outline" size="large"></ion-icon>
|
||||
|
||||
@@ -95,6 +95,7 @@ export class WifiPage {
|
||||
handler: async (value: { ssid: string, password: string }) => {
|
||||
await this.saveAndConnect(value.ssid, value.password)
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -458,7 +458,7 @@ export module Mock {
|
||||
'config': null,
|
||||
'critical': true,
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
'version': '>=0.2.2',
|
||||
'description': 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.',
|
||||
'requirement': {
|
||||
@@ -472,7 +472,7 @@ export module Mock {
|
||||
}
|
||||
|
||||
export const MockManifestBitcoinProxy: Manifest = {
|
||||
id: 'bitcoin-proxy',
|
||||
id: 'btc-rpc-proxy',
|
||||
title: 'Bitcoin Proxy',
|
||||
version: '0.2.2',
|
||||
description: {
|
||||
@@ -661,9 +661,9 @@ export module Mock {
|
||||
title: 'Bitcoin Core',
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
icon: 'assets/img/service-icons/bitcoin-proxy.png',
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -683,9 +683,9 @@ export module Mock {
|
||||
title: 'Bitcoin Core',
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
icon: 'assets/img/service-icons/bitcoin-proxy.png',
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -701,16 +701,16 @@ export module Mock {
|
||||
title: 'Bitcoin Core',
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
icon: 'assets/img/service-icons/bitcoin-proxy.png',
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
'latest': {
|
||||
icon: 'assets/img/service-icons/bitcoin-proxy.png',
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
license: 'licenseUrl',
|
||||
instructions: 'instructionsUrl',
|
||||
manifest: Mock.MockManifestBitcoinProxy,
|
||||
@@ -1565,9 +1565,9 @@ export module Mock {
|
||||
export const bitcoinProxy: PackageDataEntry = {
|
||||
state: PackageState.Installed,
|
||||
'static-files': {
|
||||
'license': '/public/package-data/bitcoin-proxy/0.20.0/LICENSE.md',
|
||||
'icon': '/assets/img/service-icons/bitcoin-proxy.png',
|
||||
'instructions': '/public/package-data/bitcoin-proxy/0.20.0/INSTRUCTIONS.md',
|
||||
'license': '/public/package-data/btc-rpc-proxy/0.20.0/LICENSE.md',
|
||||
'icon': '/assets/img/service-icons/btc-rpc-proxy.png',
|
||||
'instructions': '/public/package-data/btc-rpc-proxy/0.20.0/INSTRUCTIONS.md',
|
||||
},
|
||||
manifest: MockManifestBitcoinProxy,
|
||||
installed: {
|
||||
@@ -1629,7 +1629,7 @@ export module Mock {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
type: DependencyErrorType.NotInstalled,
|
||||
},
|
||||
},
|
||||
@@ -1652,7 +1652,7 @@ export module Mock {
|
||||
pointers: [],
|
||||
'health-checks': [],
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
pointers: [],
|
||||
'health-checks': [],
|
||||
},
|
||||
@@ -1662,9 +1662,9 @@ export module Mock {
|
||||
manifest: Mock.MockManifestBitcoind,
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
},
|
||||
'bitcoin-proxy': {
|
||||
'btc-rpc-proxy': {
|
||||
manifest: Mock.MockManifestBitcoinProxy,
|
||||
icon: 'assets/img/service-icons/bitcoin-proxy.png',
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1673,7 +1673,7 @@ export module Mock {
|
||||
|
||||
export const LocalPkgs: { [key: string]: PackageDataEntry } = {
|
||||
'bitcoind': bitcoind,
|
||||
'bitcoin-proxy': bitcoinProxy,
|
||||
'btc-rpc-proxy': bitcoinProxy,
|
||||
'lnd': lnd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,6 @@ export module RR {
|
||||
export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create
|
||||
export type CreateBackupRes = WithRevision<null>
|
||||
|
||||
export type RestoreBackupReq = { logicalname: string, password: string } // backup.restore - unauthed
|
||||
export type RestoreBackupRes = null
|
||||
|
||||
// disk
|
||||
|
||||
export type GetDisksReq = { } // disk.list
|
||||
|
||||
@@ -119,11 +119,6 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
() => this.createBackupRaw(params),
|
||||
)()
|
||||
|
||||
protected abstract restoreBackupRaw (params: RR.RestoreBackupReq): Promise<RR.RestoreBackupRes>
|
||||
restoreBackup = (params: RR.RestoreBackupReq) => this.syncResponse(
|
||||
() => this.restoreBackupRaw(params),
|
||||
)()
|
||||
|
||||
// disk
|
||||
|
||||
abstract getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes>
|
||||
|
||||
@@ -191,10 +191,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
async restoreBackupRaw (params: RR.RestoreBackupReq): Promise <RR.RestoreBackupRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.restore', params })
|
||||
}
|
||||
|
||||
// disk
|
||||
|
||||
getDisks (params: RR.GetDisksReq): Promise <RR.GetDisksRes> {
|
||||
|
||||
@@ -282,11 +282,6 @@ export class MockApiService extends ApiService {
|
||||
return res
|
||||
}
|
||||
|
||||
async restoreBackupRaw (params: RR.RestoreBackupReq): Promise<RR.RestoreBackupRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// disk
|
||||
|
||||
async getDisks (params: RR.GetDisksReq): Promise<RR.GetDisksRes> {
|
||||
@@ -482,17 +477,27 @@ export class MockApiService extends ApiService {
|
||||
value: PackageState.Removing,
|
||||
},
|
||||
]
|
||||
|
||||
const res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
setTimeout(async () => {
|
||||
let res: any
|
||||
try {
|
||||
res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
setTimeout(async () => {
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${params.id}`,
|
||||
},
|
||||
]
|
||||
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
}, this.revertTime)
|
||||
} catch (e) {
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${params.id}`,
|
||||
path: `/recovered-packages/${params.id}`,
|
||||
},
|
||||
]
|
||||
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
}, this.revertTime)
|
||||
res = await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -540,14 +545,32 @@ export class MockApiService extends ApiService {
|
||||
if (i === initialProgress.size) {
|
||||
initialProgress[phase.completion] = true
|
||||
}
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
value: initialProgress,
|
||||
},
|
||||
]
|
||||
await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
let patch: any
|
||||
if (initialProgress['unpack-complete']) {
|
||||
patch = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/recovered-packages/${id}`,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${id}/install-progress`,
|
||||
value: initialProgress,
|
||||
},
|
||||
]
|
||||
}
|
||||
try {
|
||||
await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
} catch (e) {
|
||||
console.error('Insufficient Mocks, happens when installing a service that does not exist in recovered-package')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,12 +615,7 @@ export class MockApiService extends ApiService {
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status',
|
||||
value: ServerStatus.Running,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/version',
|
||||
value: '3.1.0',
|
||||
value: ServerStatus.Updated,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
@@ -606,16 +624,16 @@ export class MockApiService extends ApiService {
|
||||
]
|
||||
|
||||
await this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
|
||||
// quickly revert patch to proper version to prevent infinite refresh loop
|
||||
// quickly revert server to "running" for continued testing
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/version',
|
||||
value: require('../../../../package.json').version,
|
||||
path: '/server-info/status',
|
||||
value: ServerStatus.Running,
|
||||
},
|
||||
]
|
||||
this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch: patch2 } })
|
||||
}, 10000)
|
||||
}, 1000)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
export interface DataModel {
|
||||
'server-info': ServerInfo
|
||||
'package-data': { [id: string]: PackageDataEntry }
|
||||
'recovered-packages': { [id: string]: RecoveredPackageDataEntry }
|
||||
ui: UIData
|
||||
}
|
||||
|
||||
@@ -10,6 +11,7 @@ export interface UIData {
|
||||
name: string
|
||||
'welcome-ack': string
|
||||
'auto-check-updates': boolean
|
||||
'pkg-order': string[]
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
@@ -30,8 +32,14 @@ export interface ServerInfo {
|
||||
export enum ServerStatus {
|
||||
Running = 'running',
|
||||
Updating = 'updating',
|
||||
Updated = 'updated',
|
||||
BackingUp = 'backing-up',
|
||||
}
|
||||
export interface RecoveredPackageDataEntry {
|
||||
title: string,
|
||||
icon: URL,
|
||||
version: string,
|
||||
}
|
||||
|
||||
export interface PackageDataEntry {
|
||||
state: PackageState
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Subject, BehaviorSubject } from 'rxjs'
|
||||
import { PropertySubject, initPropertySubject, complete, peekProperties, PropertySubjectId } from './property-subject.util'
|
||||
import { NgZone } from '@angular/core'
|
||||
import { both, diff } from './misc.util'
|
||||
|
||||
export type Update<T extends { id: string }> = Partial<T> & {
|
||||
id: string
|
||||
}
|
||||
export type Delta<T> = { action: 'add' | 'delete', id: string } | { action: 'update', id: string, effectedFields: Partial<T> }
|
||||
|
||||
export class MapSubject<T extends { id: string }> {
|
||||
contents: { [id: string]: PropertySubject<T> } = { }
|
||||
$delta$ = new Subject<Delta<T>>()
|
||||
|
||||
constructor (
|
||||
private readonly zone: NgZone = new NgZone({ shouldCoalesceEventChangeDetection: true }),
|
||||
) { }
|
||||
|
||||
get ids () : string[] { return Object.keys(this.contents) }
|
||||
get all () : T[] { return this.ids.map(id => this.peek(id) as T) }
|
||||
|
||||
getContents () : PropertySubjectId<T>[] {
|
||||
return Object.entries(this.contents).map( ([k, v]) => ({ id: k, subject: v }))
|
||||
}
|
||||
|
||||
add (t: T): void {
|
||||
this.contents[t.id] = initPropertySubject(t)
|
||||
this.$delta$.next({ action: 'add', id: t.id })
|
||||
}
|
||||
|
||||
delete (id: string): void {
|
||||
const t$ = this.contents[id]
|
||||
if (!t$) return
|
||||
complete(t$)
|
||||
delete this.contents[id]
|
||||
this.$delta$.next({ action: 'delete', id })
|
||||
}
|
||||
|
||||
update (newValues: Update<T>): void {
|
||||
const t$ = this.contents[newValues.id] as PropertySubject<T>
|
||||
|
||||
if (!t$) {
|
||||
this.contents[newValues.id] = initPropertySubject(newValues) as PropertySubject<T>
|
||||
return
|
||||
}
|
||||
|
||||
const effectedFields = { }
|
||||
const oldKeys = Object.keys(t$)
|
||||
const newKeys = Object.keys(newValues)
|
||||
|
||||
const newKeysInUpdate = diff(newKeys, oldKeys)
|
||||
newKeysInUpdate.forEach(keyToAdd => {
|
||||
t$[keyToAdd] = new BehaviorSubject(newValues[keyToAdd])
|
||||
effectedFields[keyToAdd] = newValues[keyToAdd]
|
||||
})
|
||||
|
||||
const keysToUpdate = both(newKeys, oldKeys)
|
||||
keysToUpdate.forEach(keyToUpdate => {
|
||||
const valueToUpdate = newValues[keyToUpdate]
|
||||
if (JSON.stringify(t$[keyToUpdate].getValue()) !== JSON.stringify(valueToUpdate)) {
|
||||
this.zone.run(() => t$[keyToUpdate].next(valueToUpdate))
|
||||
effectedFields[keyToUpdate] = newValues[keyToUpdate]
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(effectedFields).length) {
|
||||
this.$delta$.next({
|
||||
action: 'update',
|
||||
id: newValues.id,
|
||||
effectedFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch (id: string): undefined | PropertySubject<T> {
|
||||
return this.contents[id]
|
||||
}
|
||||
|
||||
peek (id: string): T | undefined {
|
||||
return this.contents[id] && peekProperties(this.contents[id])
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Reference in New Issue
Block a user