This commit is contained in:
Matt Hill
2021-10-11 11:15:12 -06:00
committed by Aiden McClelland
parent 36f0959bc2
commit e864d0eb64
22 changed files with 18665 additions and 1268 deletions

19090
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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 () { }
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -95,6 +95,7 @@ export class WifiPage {
handler: async (value: { ssid: string, password: string }) => {
await this.saveAndConnect(value.ssid, value.password)
},
isSubmit: true,
},
],
},

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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> {

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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])
}
}

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB