mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
0.3.0 refactor
ui: adds overlay layer to patch-db-client ui: getting towards mocks ui: cleans up factory init ui: nice type hack ui: live api for patch ui: api service source + http starts up ui: api source + http ui: rework patchdb config, pass stashTimeout into patchDbModel wires in temp patching into api service ui: example of wiring patchdbmodel into page begin integration remove unnecessary method linting first data rendering rework app initialization http source working for ssh delete call temp patches working entire Embassy tab complete not in kansas anymore ripping, saving progress progress for API request response types and endoint defs Update data-model.ts shambles, but in a good way progress big progress progress installed list working big progress progress progress begin marketplace redesign Update api-types.ts Update api-types.ts marketplace improvements cosmetic dependencies and recommendations begin nym auth approach install wizard restore flow and donations
This commit is contained in:
committed by
Aiden McClelland
parent
46f32cb90b
commit
8d01ebe8b2
@@ -8,34 +8,25 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
<ng-container *ngIf="patch.watch$('package-data', pkgId, 'installed') | ngrxPush as installed">
|
||||
<ng-container *ngIf="installed.manifest as manifest">
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async) && {
|
||||
title: app.title | async,
|
||||
versionInstalled: app.versionInstalled | async,
|
||||
status: app.status | async,
|
||||
actions: app.actions | async
|
||||
} as vars">
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- no metrics -->
|
||||
<ion-item *ngIf="!vars.actions.length">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No Actions for {{ vars.title }} {{ vars.versionInstalled }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- actions -->
|
||||
<ion-item-group>
|
||||
<ion-item button *ngFor="let action of vars.actions" (click)="handleAction(action)" >
|
||||
<ion-item *ngIf="manifest.actions | empty; else actions">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="primary">{{ action.name }}</ion-text><ion-icon *ngIf="!(action.allowedStatuses | includes: vars.status)" color="danger" name="close-outline"></ion-icon></h2>
|
||||
<p><ion-text color="dark">{{ action.description }}</ion-text></p>
|
||||
<p>No Actions for {{ manifest.title }} {{ manifest.versionInstalled }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
|
||||
<ng-template #actions>
|
||||
<ion-item-group>
|
||||
<ion-item button *ngFor="let action of manifest.actions | keyvalue: asIsOrder" (click)="handleAction(installed, action)" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="primary">{{ action.value.name }}</ion-text><ion-icon *ngIf="!(action.value['allowed-statuses'] | includes: installed.status.main.status)" color="danger" name="close-outline"></ion-icon></h2>
|
||||
<p><ion-text color="dark">{{ action.value.description }}</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
@@ -1,49 +1,37 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService, isRpcFailure, isRpcSuccess } from 'src/app/services/api/api.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { ServiceAction, AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { AppStatus } from 'src/app/models/app-model'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { Action, InstalledPackageDataEntry, PackageMainStatus } from 'src/app/models/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './app-actions.page.html',
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
})
|
||||
export class AppActionsPage extends Cleanup {
|
||||
error = ''
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
appId: string
|
||||
app: PropertySubject<AppInstalledFull>
|
||||
export class AppActionsPage {
|
||||
pkgId: string
|
||||
|
||||
constructor(
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly loaderService: LoaderService,
|
||||
) { super() }
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId')
|
||||
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).pipe(
|
||||
map(app => this.app = app),
|
||||
).subscribe({ error: e => this.error = e.message })
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
}
|
||||
|
||||
async handleAction(action: ServiceAction) {
|
||||
if (action.allowedStatuses.includes(this.app.status.getValue())) {
|
||||
async handleAction (pkg: InstalledPackageDataEntry, action: { key: string, value: Action }) {
|
||||
if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.status.main.status)) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to execute action "${action.name}"? ${action.warning ? action.warning : ""}`,
|
||||
message: `Are you sure you want to execute action "${action.value.name}"? ${action.value.warning || ''}`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
@@ -52,7 +40,7 @@ export class AppActionsPage extends Cleanup {
|
||||
{
|
||||
text: 'Execute',
|
||||
handler: () => {
|
||||
this.executeAction(action)
|
||||
this.executeAction(pkg.manifest.id, action.key)
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -83,25 +71,19 @@ export class AppActionsPage extends Cleanup {
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(action: ServiceAction) {
|
||||
private async executeAction (pkgId: string, actionId: string) {
|
||||
try {
|
||||
const res = await this.loaderService.displayDuringP(
|
||||
this.apiService.serviceAction(this.appId, action),
|
||||
this.apiService.executePackageAction({ id: pkgId, 'action-id': actionId }),
|
||||
)
|
||||
|
||||
if (isRpcFailure(res)) {
|
||||
this.presentAlertActionFail(res.error.code, res.error.message)
|
||||
}
|
||||
|
||||
if (isRpcSuccess(res)) {
|
||||
const successAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Complete',
|
||||
message: res.result.split('\n').join('</br ></br />'),
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-success-message',
|
||||
})
|
||||
return await successAlert.present()
|
||||
}
|
||||
const successAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Complete',
|
||||
message: res.message.split('\n').join('</br ></br />'),
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-success-message',
|
||||
})
|
||||
return await successAlert.present()
|
||||
} catch (e) {
|
||||
if (e instanceof HttpErrorResponse) {
|
||||
this.presentAlertActionFail(e.status, e.message)
|
||||
@@ -111,7 +93,7 @@ export class AppActionsPage extends Cleanup {
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertActionFail(code: number, message: string): Promise<void> {
|
||||
private async presentAlertActionFail (code: number, message: string): Promise<void> {
|
||||
const failureAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Failed',
|
||||
message: `Error code ${code}. ${message}`,
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppAvailableListPage } from './app-available-list.page'
|
||||
import { SharingModule } from '../../../modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { UpdateOsBannerComponentModule } from 'src/app/components/update-os-banner/update-os-banner.component.module'
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -24,9 +22,7 @@ const routes: Routes = [
|
||||
RouterModule.forChild(routes),
|
||||
StatusComponentModule,
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
UpdateOsBannerComponentModule,
|
||||
],
|
||||
declarations: [AppAvailableListPage],
|
||||
})
|
||||
|
||||
@@ -1,74 +1,87 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Service Marketplace</ion-title>
|
||||
<ion-title>Embassy Marketplace</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
<update-os-banner></update-os-banner>
|
||||
<ion-toolbar *ngIf="!pageLoading">
|
||||
<ion-searchbar (ionChange)="search($event)" debounce="400"></ion-searchbar>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-bottom">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<ion-content class="ion-padding-top" *ngrxLet="patch.watch$('package-data') as installedPkgs">
|
||||
<ion-spinner *ngIf="pageLoading; else pageLoaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ng-template #pageLoaded>
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
|
||||
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<b>Get ready. View the update instructions.</b>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
<div class="scrollable">
|
||||
<ion-button
|
||||
*ngFor="let cat of data.categories"
|
||||
size="small"
|
||||
fill="clear"
|
||||
[color]="cat === category ? 'success' : 'dark'"
|
||||
(click)="switchCategory(cat)"
|
||||
>
|
||||
{{ cat }}
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
|
||||
<ion-card *ngIf="eos && category === 'featured'" class="eos-card" (click)="updateEos()">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Now Available...</ion-card-subtitle>
|
||||
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
||||
<ion-card-title>EmbassyOS Version {{ eos.version }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<b>View the update instructions.</b>
|
||||
{{ eos.headline }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
|
||||
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
|
||||
<ion-avatar style="margin-top: 8px;" slot="start">
|
||||
<img [src]="app.subject.iconURL | async | iconParse" />
|
||||
</ion-avatar>
|
||||
<ion-label style="margin-top: 6px; margin-bottom: 3px">
|
||||
<h1 style="font-family: 'Montserrat'; font-size: 20px; margin-bottom: 0px;">
|
||||
{{app.subject.title | async}}
|
||||
</h1>
|
||||
<div *ngIf="!l.installing && l.installComparison === 'installed-equal'" class="beneath-title">
|
||||
<ion-text style="font-size: 12px;" color="success">Installed</ion-text>
|
||||
</div>
|
||||
<div *ngIf="!l.installing && l.installComparison === 'installed-below'" class="beneath-title">
|
||||
<ion-text style="font-size: 12px;" color="warning">Update Available</ion-text>
|
||||
</div>
|
||||
<div *ngIf="l.installing" class="beneath-title" style="display: flex; flex-direction: row; align-items: center;">
|
||||
<ion-text style="font-size: 12px;" color="primary">Installing</ion-text>
|
||||
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-card-content style="
|
||||
font-size: small !important;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 6px;
|
||||
">
|
||||
{{ app.subject.descriptionShort | async }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
<ion-spinner *ngIf="pkgsLoading; else pkgsLoaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-template #pkgsLoaded>
|
||||
<ion-card *ngFor="let pkg of pkgs" style="margin: 10px 10px;" [routerLink]="[pkg.id]">
|
||||
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;">
|
||||
<ion-avatar style="margin-top: 8px;" slot="start">
|
||||
<img [src]="pkg.icon" />
|
||||
</ion-avatar>
|
||||
<ion-label style="margin-top: 6px; margin-bottom: 3px">
|
||||
<h1 style="font-family: 'Montserrat'; font-size: 20px; margin-bottom: 0px;">
|
||||
{{ pkg.title }}
|
||||
</h1>
|
||||
<p>{{ pkg.version }}</p>
|
||||
<div class="beneath-title" *ngIf="installedPkgs[pkg.id] as pkgI">
|
||||
<ng-container *ngIf="pkgI.state === PackageState.Installed">
|
||||
<ion-text *ngIf="(pkg.version | compareEmver : pkgI.installed.manifest.version) === 0" style="font-size: 12px;" color="success">Installed</ion-text>
|
||||
<ion-text *ngIf="(pkg.version | compareEmver : pkgI.installed.manifest.version) === 1" style="font-size: 12px;" color="warning">Update Available</ion-text>
|
||||
</ng-container>
|
||||
<div class="beneath-title" *ngIf="pkgI.state === PackageState.Installing" style="display: flex; flex-direction: row; align-items: center;">
|
||||
<ion-text style="font-size: 12px;" color="primary">Installing</ion-text>
|
||||
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
|
||||
</div>
|
||||
<div class="beneath-title" *ngIf="pkgI.state === PackageState.Updating" style="display: flex; flex-direction: row; align-items: center;">
|
||||
<ion-text style="font-size: 12px;" color="primary">Updating</ion-text>
|
||||
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
|
||||
</div>
|
||||
<div class="beneath-title" *ngIf="pkgI.state === PackageState.Removing" style="display: flex; flex-direction: row; align-items: center;">
|
||||
<ion-text style="font-size: 12px;" color="danger">Removing</ion-text>
|
||||
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="danger"></ion-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-card-content style="
|
||||
font-size: small !important;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 6px;
|
||||
">
|
||||
{{ pkg.descriptionShort }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -5,12 +5,24 @@
|
||||
padding: 1px 0px 1.5px 0px;
|
||||
}
|
||||
|
||||
.instructions-card {
|
||||
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
|
||||
margin: 16px 10px;
|
||||
.scrollable {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
background-color: var(--ion-color-light);
|
||||
margin-bottom: 16px;
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.available-card {
|
||||
.eos-card {
|
||||
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
|
||||
margin: 16px 10px;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Component, NgZone } from '@angular/core'
|
||||
import { Component } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AppModel } from 'src/app/models/app-model'
|
||||
import { AppAvailablePreview, AppInstalledPreview } from 'src/app/models/app-types'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { PropertySubjectId, initPropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { V1Status } from 'src/app/services/api/api-types'
|
||||
import { MarketplaceData, MarketplaceEOS, AvailablePreview } from 'src/app/services/api/api-types'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { PackageState } from 'src/app/models/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-available-list',
|
||||
@@ -16,83 +13,93 @@ import { V1Status } from 'src/app/services/api/api-types'
|
||||
styleUrls: ['./app-available-list.page.scss'],
|
||||
})
|
||||
export class AppAvailableListPage {
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
pageLoading = true
|
||||
pkgsLoading = true
|
||||
error = ''
|
||||
installedAppDeltaSubscription: Subscription
|
||||
apps: PropertySubjectId<AppAvailablePreview>[] = []
|
||||
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
|
||||
v1Status: V1Status = { status: 'nothing', version: '' }
|
||||
|
||||
category = 'featured'
|
||||
query: string
|
||||
|
||||
data: MarketplaceData
|
||||
eos: MarketplaceEOS
|
||||
pkgs: AvailablePreview[] = []
|
||||
|
||||
PackageState = PackageState
|
||||
|
||||
page = 1
|
||||
needInfinite = false
|
||||
readonly perPage = 20
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly zone: NgZone,
|
||||
private readonly osUpdateService: OsUpdateService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
public patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.installedAppDeltaSubscription = this.appModel
|
||||
.watchDelta('update')
|
||||
.subscribe(({ id }) => this.mergeInstalledProps(id))
|
||||
|
||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
||||
this.getApps(),
|
||||
this.checkV1Status(),
|
||||
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
|
||||
pauseFor(600),
|
||||
]))
|
||||
}
|
||||
|
||||
ionViewDidEnter () {
|
||||
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
||||
}
|
||||
|
||||
async checkV1Status () {
|
||||
try {
|
||||
this.v1Status = await this.apiService.checkV1Status()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
mergeInstalledProps (appInstalledId: string) {
|
||||
const appAvailable = this.apps.find(app => app.id === appInstalledId)
|
||||
if (!appAvailable) return
|
||||
|
||||
const app = this.appModel.watch(appInstalledId)
|
||||
combineLatest([app.status, app.versionInstalled])
|
||||
.pipe(take(1))
|
||||
.subscribe(([status, versionInstalled]) => {
|
||||
this.zone.run(() => {
|
||||
appAvailable.subject.status.next(status)
|
||||
appAvailable.subject.versionInstalled.next(versionInstalled)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.installedAppDeltaSubscription.unsubscribe()
|
||||
}
|
||||
|
||||
async doRefresh (e: any) {
|
||||
await Promise.all([
|
||||
this.getApps(),
|
||||
pauseFor(600),
|
||||
])
|
||||
e.target.complete()
|
||||
}
|
||||
|
||||
async getApps (): Promise<void> {
|
||||
try {
|
||||
this.apps = await this.apiService.getAvailableApps().then(apps =>
|
||||
apps
|
||||
.sort( (a1, a2) => a2.latestVersionTimestamp.getTime() - a1.latestVersionTimestamp.getTime())
|
||||
.map(a => ({ id: a.id, subject: initPropertySubject(a) })),
|
||||
)
|
||||
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
||||
const [data, eos, pkgs] = await Promise.all([
|
||||
this.apiService.getMarketplaceData({ }),
|
||||
this.apiService.getEos({ }),
|
||||
this.getPkgs(),
|
||||
])
|
||||
this.data = data
|
||||
this.eos = eos
|
||||
this.pkgs = pkgs
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.pageLoading = false
|
||||
this.pkgsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async doInfinite (e: any): Promise<void> {
|
||||
const pkgs = await this.getPkgs()
|
||||
this.pkgs = this.pkgs.concat(pkgs)
|
||||
e.target.complete()
|
||||
}
|
||||
|
||||
async search (e?: any): Promise<void> {
|
||||
this.query = e.target.value || undefined
|
||||
this.page = 1
|
||||
this.pkgs = await this.getPkgs()
|
||||
}
|
||||
|
||||
async updateEos (): Promise<void> {
|
||||
await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.updateOS({
|
||||
version: this.eos.version,
|
||||
releaseNotes: this.eos.notes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async getPkgs (): Promise<AvailablePreview[]> {
|
||||
this.pkgsLoading = true
|
||||
try {
|
||||
const pkgs = await this.apiService.getAvailableList({
|
||||
category: this.category,
|
||||
query: this.query,
|
||||
page: this.page,
|
||||
'per-page': this.perPage,
|
||||
})
|
||||
this.needInfinite = pkgs.length >= this.perPage
|
||||
this.page++
|
||||
return pkgs
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.pkgsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async switchCategory (category: string): Promise<void> {
|
||||
this.category = category
|
||||
this.pkgs = await this.getPkgs()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { DependencyListComponentModule } from '../../../components/dependency-list/dependency-list.component.module'
|
||||
import { AppAvailableShowPage } from './app-available-show.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
@@ -10,7 +9,6 @@ import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/b
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
|
||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -25,14 +23,12 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
StatusComponentModule,
|
||||
DependencyListComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
RecommendationButtonComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
InstallWizardComponentModule,
|
||||
ErrorMessageComponentModule,
|
||||
InformationPopoverComponentModule,
|
||||
],
|
||||
declarations: [AppAvailableShowPage],
|
||||
|
||||
@@ -10,109 +10,140 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-bottom" *ngIf="{
|
||||
id: $app$.id | async,
|
||||
status: $app$.status | async,
|
||||
title: $app$.title | async,
|
||||
versionInstalled: $app$.versionInstalled | async,
|
||||
versionViewing: $app$.versionViewing | async,
|
||||
descriptionLong: $app$.descriptionLong | async,
|
||||
licenseName: $app$.licenseName | async,
|
||||
licenseLink: $app$.licenseLink | async,
|
||||
serviceRequirements: $app$.serviceRequirements | async,
|
||||
iconURL: $app$.iconURL | async,
|
||||
releaseNotes: $app$.releaseNotes | async
|
||||
} as vars"
|
||||
>
|
||||
<ion-spinner *ngIf="($loading$ | async)" class="center" name="lines" color="warning"></ion-spinner>
|
||||
<ion-content class="ion-padding-bottom">
|
||||
|
||||
<error-message [$error$]="$error$" [dismissable]="vars.id"></error-message>
|
||||
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async) && vars.id && ($app$ | compareInstalledAndViewing | async) as installedStatus">
|
||||
<ion-item-group>
|
||||
<ion-item lines="none">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="vars.iconURL | iconParse" />
|
||||
</ion-avatar>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h1 style="font-family: 'Montserrat'">{{ vars.title }}</h1>
|
||||
<h3>{{ vars.versionViewing | displayEmver }}</h3>
|
||||
<ng-container *ngIf="vars.status !== 'INSTALLING'">
|
||||
<h3 *ngIf="installedStatus === 'installed-equal'"><ion-text color="medium">Installed</ion-text></h3>
|
||||
<h3 *ngIf="installedStatus === 'installed-below' || installedStatus === 'installed-above'"><ion-text color="medium">Installed </ion-text><ion-text style="font-size: small" color="medium"> at {{vars.versionInstalled | displayEmver}}</ion-text></h3>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="vars.status === 'INSTALLING'">
|
||||
<h3>
|
||||
<status appStatus="INSTALLING" [text]="' (' + (vars.versionInstalled | displayEmver) + ')'" size="medium"></status>
|
||||
</h3>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
<ng-template #loaded>
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-button *ngIf="!vars.versionInstalled" class="main-action-button" expand="block" fill="outline" color="success" (click)="install()">
|
||||
Install
|
||||
</ion-button>
|
||||
|
||||
<div *ngIf="vars.versionInstalled">
|
||||
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', vars.id]">
|
||||
Go to Service
|
||||
</ion-button>
|
||||
<div *ngIf="vars.status !== 'INSTALLING' ">
|
||||
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||
Update to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||
Downgrade to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-item-group>
|
||||
<ng-container *ngIf="recommendation">
|
||||
<ion-item class="recommendation-item">
|
||||
<ng-container *ngrxLet="patch.watch$('package-data', pkgId) as localPkg">
|
||||
<ion-item-group>
|
||||
<ion-item lines="none">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="pkg.icon" />
|
||||
</ion-avatar>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<ion-avatar style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
||||
<img [src]="recommendation.iconURL | iconParse" [alt]="recommendation.title"/>
|
||||
</ion-avatar>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{recommendation.title}}</ion-text>
|
||||
</h2>
|
||||
<div style="margin: 2px 5px">
|
||||
<p style="color: var(--ion-color-medium); font-size: small">{{recommendation.description}}</p>
|
||||
<p *ngIf="vars.versionViewing | satisfiesEmver: recommendation.versionSpec" class="recommendation-text">{{vars.title}} version {{vars.versionViewing | displayEmver}} is compatible.</p>
|
||||
<p *ngIf="!(vars.versionViewing | satisfiesEmver: recommendation.versionSpec)" class="recommendation-text recommendation-error">{{vars.title}} version {{vars.versionViewing | displayEmver}} is NOT compatible.</p>
|
||||
</div>
|
||||
<h1 style="font-family: 'Montserrat'">{{ pkg.manifest.title }}</h1>
|
||||
<h3>{{ pkg.manifest.version | displayEmver }}</h3>
|
||||
<!-- no localPkg -->
|
||||
<h3 *ngIf="!localPkg; else local">
|
||||
<ion-text color="medium">Not Installed</ion-text>
|
||||
</h3>
|
||||
<!-- localPkg -->
|
||||
<ng-template #local>
|
||||
<h3 *ngIf="localPkg.state !== PackageState.Installed; else installed">
|
||||
<!-- installing, updating, removing -->
|
||||
<ion-text [color]="localPkg.state === PackageState.Removing ? 'danger' : 'primary'">{{ localPkg.state }}</ion-text>
|
||||
<ion-spinner class="dots dots-medium" name="dots" [color]="localPkg.state === PackageState.Removing ? 'danger' : 'primary'"></ion-spinner>
|
||||
</h3>
|
||||
<!-- installed -->
|
||||
<ng-template #installed>
|
||||
<h3>
|
||||
<ion-text color="medium">Installed at {{ localPkg.installed.manifest.version | displayEmver }}</ion-text>
|
||||
</h3>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- no localPkg -->
|
||||
<ion-button *ngIf="!localPkg; else localPkg2" class="main-action-button" expand="block" fill="outline" color="success" (click)="install()">
|
||||
Install
|
||||
</ion-button>
|
||||
|
||||
<!-- localPkg -->
|
||||
<ng-template #localPkg2>
|
||||
<!-- not removing -->
|
||||
<ng-container *ngIf="localPkg.state !== PackageState.Removing">
|
||||
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', pkgId]">
|
||||
Go to Service
|
||||
</ion-button>
|
||||
<!-- not installing or updating -->
|
||||
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
||||
<ion-button *ngIf="(localPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === -1" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||
Update to {{ pkg.manifest.version | displayEmver }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="(localPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === 1" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||
Downgrade to {{ pkg.manifest.version | displayEmver }}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ vars.versionViewing | displayEmver }}</ion-item-divider>
|
||||
<!-- recommendation -->
|
||||
<ion-item *ngIf="rec && showRec" class="rec-item">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<ion-avatar style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||
</ion-avatar>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||
</h2>
|
||||
<div style="margin: 7px 5px;">
|
||||
<p style="color: var(--ion-color-dark); font-size: small">{{ rec.description }}</p>
|
||||
<p *ngIf="pkg.manifest.version | satisfiesEmver: rec.version" class="recommendation-text">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is compatible.</p>
|
||||
<p *ngIf="!(pkg.manifest.version | satisfiesEmver: rec.version)" class="recommendation-text recommendation-error">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.</p>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-group>
|
||||
<!-- release notes -->
|
||||
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ pkg.manifest.version | displayEmver }}</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label *ngIf="!($newVersionLoading$ | async)" style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
|
||||
<div id='release-notes'color="dark" [innerHTML]="vars.releaseNotes | markdown"></div>
|
||||
<ion-label style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
|
||||
<div id='release-notes' color="dark" [innerHTML]="pkg.manifest['release-notes'] | markdown"></div>
|
||||
</ion-label>
|
||||
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider class="divider">Description</ion-item-divider>
|
||||
<!-- description -->
|
||||
<ion-item-divider class="divider">
|
||||
<ion-text color="dark">Description</ion-text>
|
||||
</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<ion-text color="medium">
|
||||
<h5>{{ vars.descriptionLong }}</h5>
|
||||
<ion-text color="dark">
|
||||
<h5>{{ pkg.manifest.description.long }}</h5>
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="(vars.serviceRequirements)?.length">
|
||||
<ion-item-divider class="divider">Service Dependencies
|
||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(serviceDependencyDefintion, $event)">
|
||||
<!-- dependencies -->
|
||||
<ng-container *ngIf="!(pkg.manifest.dependencies | empty)">
|
||||
<ion-item-divider class="divider">
|
||||
<ion-text color="dark">Service Dependencies</ion-text>
|
||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="dark" (click)="presentPopover(depDefintion, $event)">
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
|
||||
|
||||
<div *ngFor="let dep of pkg.manifest.dependencies | keyvalue">
|
||||
<ion-item *ngIf="!dep.value.optional" class="dependency-item">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="pkg['dependency-metadata'][dep.key].icon" />
|
||||
</ion-avatar>
|
||||
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
|
||||
<h4 style="font-family: 'Montserrat'">
|
||||
{{ pkg['dependency-metadata'][dep.key].title }}
|
||||
<span *ngIf="dep.value.recommended" style="font-family: 'Open Sans'; font-size: small; color: var(--ion-color-dark)"> (recommended)</span>
|
||||
</h4>
|
||||
<p style="font-size: small">{{ dep.value.version | displayEmver }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item style="margin-bottom: 10px" *ngIf="dep.value.description" lines="none">
|
||||
<div style="font-size: small; color: var(--ion-color-dark)" [innerHtml]="dep.value.description"></div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- versions -->
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
|
||||
@@ -120,9 +151,10 @@
|
||||
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item lines="none" button (click)="presentAlertVersions()">
|
||||
<ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-label>Other versions</ion-label>
|
||||
<ion-icon color="dark" slot="start" name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-label color="dark">Other versions</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -1,81 +1,64 @@
|
||||
import { Component, NgZone } from '@angular/core'
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AppAvailableFull, AppAvailableVersionSpecificInfo } from 'src/app/models/app-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs'
|
||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { AppModel } from 'src/app/models/app-model'
|
||||
import { initPropertySubject, peekProperties, PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { AvailableShow } from 'src/app/services/api/api-types'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { PackageState } from 'src/app/models/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-available-show',
|
||||
templateUrl: './app-available-show.page.html',
|
||||
styleUrls: ['./app-available-show.page.scss'],
|
||||
})
|
||||
export class AppAvailableShowPage extends Cleanup {
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
export class AppAvailableShowPage {
|
||||
loading = true
|
||||
error = ''
|
||||
pkg: AvailableShow
|
||||
pkgId: string
|
||||
|
||||
// When a new version is selected
|
||||
$newVersionLoading$ = new BehaviorSubject(false)
|
||||
// When dependencies are refreshing
|
||||
$dependenciesLoading$ = new BehaviorSubject(false)
|
||||
PackageState = PackageState
|
||||
|
||||
$error$ = new BehaviorSubject(undefined)
|
||||
$app$: PropertySubject<AppAvailableFull> = { } as any
|
||||
appId: string
|
||||
rec: Recommendation | null = null
|
||||
showRec = true
|
||||
|
||||
openRecommendation = false
|
||||
recommendation: Recommendation | null = null
|
||||
|
||||
serviceDependencyDefintion = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||
depDefinition = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly zone: NgZone,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly popoverController: PopoverController,
|
||||
private readonly emver: Emver,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
||||
|
||||
this.cleanup(
|
||||
// new version always includes dependencies, but not vice versa
|
||||
this.$newVersionLoading$.subscribe(this.$dependenciesLoading$),
|
||||
markAsLoadingDuring$(this.$loading$,
|
||||
from(this.apiService.getAvailableApp(this.appId)).pipe(
|
||||
tap(app => this.$app$ = initPropertySubject(app)),
|
||||
concatMap(() => this.fetchRecommendation()),
|
||||
),
|
||||
).pipe(
|
||||
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
|
||||
catchError(e => of(this.setError(e))),
|
||||
).subscribe(),
|
||||
)
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId') as string
|
||||
this.rec = history.state && history.state.installRec as Recommendation
|
||||
this.getPkg()
|
||||
}
|
||||
|
||||
ionViewDidEnter () {
|
||||
markAsLoadingDuring$(this.$dependenciesLoading$, this.syncVersionSpecificInfo()).subscribe({
|
||||
error: e => this.setError(e),
|
||||
})
|
||||
async getPkg (version?: string): Promise<void> {
|
||||
this.loading = true
|
||||
try {
|
||||
this.pkg = await this.apiService.getAvailableShow({ id: this.pkgId, version })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async presentPopover (information: string, ev: any) {
|
||||
@@ -92,34 +75,17 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
return await popover.present()
|
||||
}
|
||||
|
||||
syncVersionSpecificInfo (versionSpec?: string): Observable<any> {
|
||||
if (!this.$app$.versionViewing) return of({ })
|
||||
const specToFetch = versionSpec || `=${this.$app$.versionViewing.getValue()}`
|
||||
return from(this.apiService.getAvailableAppVersionSpecificInfo(this.appId, specToFetch)).pipe(
|
||||
tap(versionInfo => this.mergeInfo(versionInfo)),
|
||||
)
|
||||
}
|
||||
|
||||
private mergeInfo (versionSpecificInfo: AppAvailableVersionSpecificInfo) {
|
||||
this.zone.run(() => {
|
||||
Object.entries(versionSpecificInfo).forEach( ([k, v]) => {
|
||||
if (!this.$app$[k]) this.$app$[k] = new BehaviorSubject(undefined)
|
||||
if (v !== this.$app$[k].getValue()) this.$app$[k].next(v)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async presentAlertVersions () {
|
||||
const app = peekProperties(this.$app$)
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Versions',
|
||||
backdropDismiss: false,
|
||||
inputs: app.versions.sort((a, b) => -1 * this.emver.compare(a, b)).map(v => {
|
||||
return { name: v, // for CSS
|
||||
inputs: this.pkg.versions.sort((a, b) => -1 * this.emver.compare(a, b)).map(v => {
|
||||
return {
|
||||
name: v, // for CSS
|
||||
type: 'radio',
|
||||
label: displayEmver(v), // appearance on screen
|
||||
value: v, // literal SEM version value
|
||||
checked: app.versionViewing === v,
|
||||
checked: this.pkg.manifest.version === v,
|
||||
}
|
||||
}),
|
||||
buttons: [
|
||||
@@ -129,17 +95,7 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
}, {
|
||||
text: 'Ok',
|
||||
handler: (version: string) => {
|
||||
const previousVersion = this.$app$.versionViewing.getValue()
|
||||
this.$app$.versionViewing.next(version)
|
||||
markAsLoadingDuring$(
|
||||
this.$newVersionLoading$, this.syncVersionSpecificInfo(`=${version}`),
|
||||
)
|
||||
.subscribe({
|
||||
error: e => {
|
||||
this.setError(e)
|
||||
this.$app$.versionViewing.next(previousVersion)
|
||||
},
|
||||
})
|
||||
this.getPkg(version)
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -149,15 +105,14 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
}
|
||||
|
||||
async install () {
|
||||
const app = peekProperties(this.$app$)
|
||||
const { id, title, version, dependencies, alerts } = this.pkg.manifest
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.install({
|
||||
id: app.id,
|
||||
title: app.title,
|
||||
version: app.versionViewing,
|
||||
serviceRequirements: app.serviceRequirements,
|
||||
installAlert: app.installAlert,
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
installAlert: alerts.install,
|
||||
}),
|
||||
)
|
||||
if (cancelled) return
|
||||
@@ -166,14 +121,13 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
}
|
||||
|
||||
async update (action: 'update' | 'downgrade') {
|
||||
const app = peekProperties(this.$app$)
|
||||
|
||||
const { id, title, version, dependencies, alerts } = this.pkg.manifest
|
||||
const value = {
|
||||
id: app.id,
|
||||
title: app.title,
|
||||
version: app.versionViewing,
|
||||
serviceRequirements: app.serviceRequirements,
|
||||
installAlert: app.installAlert,
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
serviceRequirements: dependencies,
|
||||
installAlert: alerts.install,
|
||||
}
|
||||
|
||||
const { cancelled } = await wizardModal(
|
||||
@@ -188,27 +142,7 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
|
||||
private fetchRecommendation (): Observable<any> {
|
||||
this.recommendation = history.state && history.state.installationRecommendation
|
||||
|
||||
if (this.recommendation) {
|
||||
return from(this.syncVersionSpecificInfo(this.recommendation.versionSpec))
|
||||
} else {
|
||||
return of({ })
|
||||
}
|
||||
}
|
||||
|
||||
private syncWhenDependencyInstalls (): Observable<void> {
|
||||
return this.$app$.serviceRequirements.pipe(
|
||||
filter(deps => !!deps),
|
||||
switchMap(deps => this.appModel.watchForInstallations(deps)),
|
||||
concatMap(() => markAsLoadingDuring$(this.$dependenciesLoading$, this.syncVersionSpecificInfo())),
|
||||
catchError(e => of(console.error(e))),
|
||||
)
|
||||
}
|
||||
|
||||
private setError (e: Error) {
|
||||
console.error(e)
|
||||
this.$error$.next(e.message)
|
||||
dismissRec () {
|
||||
this.showRec = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import { AppConfigObjectPageModule } from 'src/app/modals/app-config-object/app-
|
||||
import { AppConfigUnionPageModule } from 'src/app/modals/app-config-union/app-config-union.module'
|
||||
import { AppConfigValuePageModule } from 'src/app/modals/app-config-value/app-config-value.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module'
|
||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||
|
||||
@@ -19,7 +17,6 @@ const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppConfigPage,
|
||||
// canDeactivate: [CanDeactivateGuard],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -35,8 +32,6 @@ const routes: Routes = [
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
RecommendationButtonComponentModule,
|
||||
InformationPopoverComponentModule,
|
||||
],
|
||||
|
||||
@@ -5,25 +5,27 @@
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ app['title'] | async }}</ion-title>
|
||||
<ion-title>{{ pkg.manifest.title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<!-- loading -->
|
||||
<div *ngIf="$loading$ | async" class="full-page-spinner">
|
||||
<ion-spinner style="justify-self: center; align-self: end;" name="lines" color="warning"></ion-spinner>
|
||||
<ion-label style="justify-self: center;" *ngIf="($loadingText$ | async)" color="dark">
|
||||
{{$loadingText$ | async}}
|
||||
</ion-label>
|
||||
</div>
|
||||
<ion-grid *ngIf="loadingText$ | ngrxPush as loadingText; else loaded" style="height: 100%;">
|
||||
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
|
||||
<ion-col>
|
||||
<ion-spinner name="lines" color="warning"></ion-spinner>
|
||||
<p>{{ loadingText }}</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- not loading -->
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ng-template #loaded>
|
||||
<ion-item *ngIf="error" class="notifier-item">
|
||||
<ion-label style="margin: 7px 5px;" class="ion-text-wrap">
|
||||
<p style="color: var(--ion-color-danger)">{{error.text}}</p>
|
||||
<p style="color: var(--ion-color-danger)">{{ error.text }}</p>
|
||||
<p><a style="color: var(--ion-color-danger); text-decoration: underline; font-weight: bold;" *ngIf="error.moreInfo && !openErrorMoreInfo" (click)="openErrorMoreInfo = true">{{error.moreInfo.buttonText}}</a></p>
|
||||
|
||||
<ng-container *ngIf="openErrorMoreInfo">
|
||||
@@ -33,44 +35,45 @@
|
||||
</ng-container>
|
||||
|
||||
</ion-label>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" *ngIf="app && (app.id | async)" color="danger" fill="clear" (click)="dismissError()">
|
||||
<ion-button style="position: absolute; right: 0; top: 0" *ngIf="pkg" color="danger" fill="clear" (click)="dismissError()">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="app && (app.id | async)">
|
||||
<ng-container *ngIf="([AppStatus.NEEDS_CONFIG] | includes: (app.status | async)) && !edited">
|
||||
<ng-container *ngIf="pkg">
|
||||
<!-- @TODO make sure this is how to determine if pkg is in needs_config -->
|
||||
<ng-container *ngIf="pkg.manifest.config && !pkg.status.configured && !edited">
|
||||
<ion-item class="notifier-item">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
||||
<ion-icon size="small" style="margin-right: 5px" slot="start" color="dark" slot="start" name="alert-circle-outline"></ion-icon>
|
||||
<ion-text style="font-size: smaller;">Initial Config</ion-text>
|
||||
</h2>
|
||||
<p style="font-size: small">To use the default config for {{ app.title | async }}, click "Save" below.</p>
|
||||
<p style="font-size: small">To use the default config for {{ app.title | ngrxPush }}, click "Save" below.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="recommendation && showRecommendation">
|
||||
<ion-item class="recommendation-item">
|
||||
<ng-container *ngIf="rec && showRec">
|
||||
<ion-item class="rec-item">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2 style="display: flex; align-items: center;">
|
||||
<ion-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
||||
<ion-avatar style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
||||
<img [src]="recommendation.iconURL | iconParse" [alt]="recommendation.title"/>
|
||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||
</ion-avatar>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{recommendation.title}}</ion-text>
|
||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||
</h2>
|
||||
<div style="margin: 7px 5px;">
|
||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{app.title | async}} config has been modified to satisfy {{recommendation.title}}.
|
||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{app.title | ngrxPush}} config has been modified to satisfy {{ rec.dependentTitle }}.
|
||||
<ion-text color="dark">To accept the changes, click “Save” below.</ion-text>
|
||||
</p>
|
||||
<a style="font-size: small" *ngIf="!openRecommendation" (click)="openRecommendation = true">More Info</a>
|
||||
<ng-container *ngIf="openRecommendation">
|
||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="recommendation.description"></p>
|
||||
<a style="font-size: x-small; font-style: italic;" (click)="openRecommendation = false">hide</a>
|
||||
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
|
||||
<ng-container *ngIf="openRec">
|
||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="rec.description"></p>
|
||||
<a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a>
|
||||
</ng-container>
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRecommendation()">
|
||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
@@ -89,18 +92,18 @@
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No config options for {{ app.title | async }} {{ app.versionInstalled | async }}.</p>
|
||||
<p>No config options for {{ app.title | ngrxPush }} {{ app.versionInstalled | ngrxPush }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- save button, always show -->
|
||||
<ion-button
|
||||
[disabled]="invalid || (!edited && !added && !(['NEEDS_CONFIG'] | includes: (app.status | async)))"
|
||||
[disabled]="invalid || (!edited && !added && !(['NEEDS_CONFIG'] | includes: (app.status | ngrxPush)))"
|
||||
fill="outline"
|
||||
expand="block"
|
||||
style="margin: 10px"
|
||||
color="primary"
|
||||
(click)="save()"
|
||||
(click)="save(pkg)"
|
||||
>
|
||||
<ion-text color="primary" style="font-weight: bold">
|
||||
Save
|
||||
@@ -115,5 +118,5 @@
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,80 +1,73 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NavController, AlertController, ModalController, PopoverController } from '@ionic/angular'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor, isEmptyObject, modulateTime } from 'src/app/util/misc.util'
|
||||
import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util'
|
||||
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { isEmptyObject } from 'src/app/util/misc.util'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { BehaviorSubject, forkJoin, from, fromEvent, of } from 'rxjs'
|
||||
import { BehaviorSubject, from, fromEvent, of, Subscription } from 'rxjs'
|
||||
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
|
||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { ConfigSpec } from 'src/app/app-config/config-types'
|
||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config',
|
||||
templateUrl: './app-config.page.html',
|
||||
styleUrls: ['./app-config.page.scss'],
|
||||
})
|
||||
export class AppConfigPage extends Cleanup {
|
||||
export class AppConfigPage {
|
||||
error: { text: string, moreInfo?:
|
||||
{ title: string, description: string, buttonText: string }
|
||||
}
|
||||
|
||||
invalid: string
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
$loadingText$ = new BehaviorSubject(undefined)
|
||||
loadingText$ = new BehaviorSubject(undefined)
|
||||
|
||||
app: PropertySubject<AppInstalledFull> = { } as any
|
||||
appId: string
|
||||
pkg: InstalledPackageDataEntry
|
||||
hasConfig = false
|
||||
|
||||
recommendation: Recommendation | null = null
|
||||
showRecommendation = true
|
||||
openRecommendation = false
|
||||
backButtonDefense = false
|
||||
|
||||
rec: Recommendation | null = null
|
||||
showRec = true
|
||||
openRec = false
|
||||
|
||||
invalid: string
|
||||
edited: boolean
|
||||
added: boolean
|
||||
rootCursor: ConfigCursor<'object'>
|
||||
spec: ConfigSpec
|
||||
config: object
|
||||
|
||||
AppStatus = AppStatus
|
||||
subs: Subscription[]
|
||||
|
||||
constructor (
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly trackingModalCtrl: TrackingModalController,
|
||||
private readonly popoverController: PopoverController,
|
||||
private readonly appModel: AppModel,
|
||||
) { super() }
|
||||
|
||||
backButtonDefense = false
|
||||
private readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
||||
const pkgId = this.route.snapshot.paramMap.get('pkgId') as string
|
||||
|
||||
this.route.params.pipe(take(1)).subscribe(params => {
|
||||
if (params.edit) {
|
||||
window.history.back()
|
||||
}
|
||||
})
|
||||
|
||||
this.cleanup(
|
||||
this.subs = [
|
||||
this.route.params.pipe(take(1)).subscribe(params => {
|
||||
if (params.edit) {
|
||||
window.history.back()
|
||||
}
|
||||
}),
|
||||
fromEvent(window, 'popstate').subscribe(() => {
|
||||
this.backButtonDefense = false
|
||||
this.trackingModalCtrl.dismissAll()
|
||||
@@ -90,49 +83,51 @@ export class AppConfigPage extends Cleanup {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
}),
|
||||
)
|
||||
]
|
||||
|
||||
markAsLoadingDuring$(this.$loading$,
|
||||
from(this.preload.appFull(this.appId))
|
||||
.pipe(
|
||||
tap(app => this.app = app),
|
||||
tap(() => this.$loadingText$.next(`Fetching config spec...`)),
|
||||
concatMap(() => forkJoin([this.apiService.getAppConfig(this.appId), pauseFor(600)])),
|
||||
concatMap(([{ spec, config }]) => {
|
||||
const rec = history.state && history.state.configRecommendation as Recommendation
|
||||
if (rec) {
|
||||
this.$loadingText$.next(`Setting properties to accomodate ${rec.title}...`)
|
||||
return from(this.apiService.postConfigureDependency(this.appId, rec.appId, true))
|
||||
.pipe(
|
||||
map(res => ({
|
||||
spec,
|
||||
config,
|
||||
dependencyConfig: res.config,
|
||||
})),
|
||||
tap(() => this.recommendation = rec),
|
||||
catchError(e => {
|
||||
this.error = { text: `Could not set properties to accomodate ${rec.title}: ${e.message}`, moreInfo: {
|
||||
title: `${rec.title} requires the following:`,
|
||||
description: rec.description,
|
||||
buttonText: 'Configure Manually',
|
||||
} }
|
||||
return of({ spec, config, dependencyConfig: null })
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this.patch.watch$('package-data', pkgId, 'installed')
|
||||
.pipe(
|
||||
tap(pkg => this.pkg = pkg),
|
||||
tap(() => this.loadingText$.next(`Fetching config spec...`)),
|
||||
concatMap(() => this.apiService.getPackageConfig({ id: pkgId })),
|
||||
concatMap(({ spec, config }) => {
|
||||
const rec = history.state && history.state.configRecommendation as Recommendation
|
||||
if (rec) {
|
||||
this.loadingText$.next(`Setting properties to accommodate ${rec.dependentTitle}...`)
|
||||
return from(this.apiService.dryConfigureDependency({ 'dependency-id': pkgId, 'dependent-id': rec.dependentId }))
|
||||
.pipe(
|
||||
map(res => ({
|
||||
spec,
|
||||
config,
|
||||
dependencyConfig: res,
|
||||
})),
|
||||
tap(() => this.rec = rec),
|
||||
catchError(e => {
|
||||
this.error = { text: `Could not set properties to accommodate ${rec.dependentTitle}: ${e.message}`, moreInfo: {
|
||||
title: `${rec.dependentTitle} requires the following:`,
|
||||
description: rec.description,
|
||||
buttonText: 'Configure Manually',
|
||||
} }
|
||||
return of({ spec, config, dependencyConfig: null })
|
||||
}
|
||||
}),
|
||||
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
|
||||
tap(() => this.$loadingText$.next(undefined)),
|
||||
),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
return of({ spec, config, dependencyConfig: null })
|
||||
}
|
||||
}),
|
||||
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
|
||||
tap(() => this.loadingText$.next(undefined)),
|
||||
take(1),
|
||||
).subscribe({
|
||||
error: e => {
|
||||
console.error(e)
|
||||
this.error = { text: e.message }
|
||||
},
|
||||
error: e => {
|
||||
console.error(e.message)
|
||||
this.error = { text: e.message }
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
async presentPopover (title: string, description: string, ev: any) {
|
||||
@@ -165,8 +160,8 @@ export class AppConfigPage extends Cleanup {
|
||||
this.hasConfig = !isEmptyObject(this.spec)
|
||||
}
|
||||
|
||||
dismissRecommendation () {
|
||||
this.showRecommendation = false
|
||||
dismissRec () {
|
||||
this.showRec = false
|
||||
}
|
||||
|
||||
dismissError () {
|
||||
@@ -181,38 +176,30 @@ export class AppConfigPage extends Cleanup {
|
||||
}
|
||||
}
|
||||
|
||||
async save () {
|
||||
const app = peekProperties(this.app)
|
||||
const ogAppStatus = app.status
|
||||
|
||||
async save (pkg: InstalledPackageDataEntry) {
|
||||
return this.loader.of({
|
||||
message: `Saving config...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync(async () => {
|
||||
const config = this.config
|
||||
const { breakages } = await this.apiService.patchAppConfig(app, config, true)
|
||||
const { breakages } = await this.apiService.drySetPackageConfig({ id: pkg.manifest.id, config: this.config })
|
||||
|
||||
if (breakages.length) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalController,
|
||||
this.wizardBaker.configure({
|
||||
app,
|
||||
pkg,
|
||||
breakages,
|
||||
}),
|
||||
)
|
||||
if (cancelled) return { skip: true }
|
||||
}
|
||||
|
||||
return this.apiService.patchAppConfig(app, config).then(
|
||||
() => this.preload.loadInstalledApp(this.appId).then(() => ({ skip: false })),
|
||||
)
|
||||
return this.apiService.setPackageConfig({ id: pkg.manifest.id, config: this.config })
|
||||
.then(() => ({ skip: false }))
|
||||
})
|
||||
.then(({ skip }) => {
|
||||
if (skip) return
|
||||
if (ogAppStatus === AppStatus.RUNNING) {
|
||||
this.appModel.update({ id: this.appId, status: AppStatus.RESTARTING }, modulateTime(new Date(), 3, 'seconds'))
|
||||
}
|
||||
this.navCtrl.back()
|
||||
})
|
||||
.catch(e => this.error = { text: e.message })
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { DependencyListComponentModule } from 'src/app/components/dependency-list/dependency-list.component.module'
|
||||
import { AppInstalledListPage } from './app-installed-list.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { AppBackupPageModule } from 'src/app/modals/app-backup/app-backup.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -23,14 +18,13 @@ const routes: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
StatusComponentModule,
|
||||
DependencyListComponentModule,
|
||||
AppBackupPageModule,
|
||||
SharingModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [AppInstalledListPage],
|
||||
declarations: [
|
||||
AppInstalledListPage,
|
||||
],
|
||||
})
|
||||
export class AppInstalledListPageModule { }
|
||||
|
||||
@@ -8,52 +8,9 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content style="position: relative">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
<ng-container *ngrxLet="patch.watch$('package-data') as pkgs">
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
|
||||
<ng-container *ngIf="{
|
||||
status: app.subject.status | async,
|
||||
hasUI: app.subject.hasUI | async,
|
||||
launchable: app.subject.launchable | async,
|
||||
iconURL: app.subject.iconURL | async | iconParse,
|
||||
title: app.subject.title | async
|
||||
} as vars">
|
||||
|
||||
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
|
||||
<div class="launch-container" *ngIf="vars.hasUI">
|
||||
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="!vars.launchable">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="vars.title" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
|
||||
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
|
||||
<ion-card-header>
|
||||
<status [appStatus]="vars.status" size="small"></status>
|
||||
<p>{{ vars.title }}</p>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<div *ngIf="!apps || !apps.length" class="ion-text-center ion-padding">
|
||||
<div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding">
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
|
||||
<h2>Welcome to your <span style="font-style: italic; color: var(--ion-color-start9)">Embassy</span></h2>
|
||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||
@@ -63,5 +20,32 @@
|
||||
Marketplace
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<ng-template #list>
|
||||
<ion-grid>
|
||||
<ion-row *ngrxLet="connectionService.monitor$() as connection">
|
||||
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
|
||||
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', (pkg.value | manifest).id]">
|
||||
<div class="launch-container" *ngIf="pkg.value | hasUi">
|
||||
<div class="launch-button-triangle" (click)="launchUi(pkg.value, $event)" [class.disabled]="!(pkg.value | isLaunchable)">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img style="position: absolute" class="main-img" [src]="pkg.value['static-files'].icon" [alt]="icon" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'green' : connection" src="assets/img/running-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'red' : connection" src="assets/img/issue-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'yellow' : connection" src="assets/img/warning-bulb.png"/>
|
||||
<img class="bulb-off" *ngIf="pkg.value | displayBulb: 'off' : connection" src="assets/img/off-bulb.png"/>
|
||||
<ion-card-header>
|
||||
<status [pkg]="pkg.value" [connection]="connection" size="small"></status>
|
||||
<p>{{ (pkg.value | manifest).title }}</p>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,126 +1,29 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { AppInstalledPreview } from 'src/app/models/app-types'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { doForAtLeast } from 'src/app/util/misc.util'
|
||||
import { PropertySubject, PropertySubjectId, toObservable } from 'src/app/util/property-subject.util'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs'
|
||||
import { S9Server, ServerModel, ServerStatus } from 'src/app/models/server-model'
|
||||
import { SyncDaemon } from 'src/app/services/sync.service'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-installed-list',
|
||||
templateUrl: './app-installed-list.page.html',
|
||||
styleUrls: ['./app-installed-list.page.scss'],
|
||||
})
|
||||
export class AppInstalledListPage extends Cleanup {
|
||||
error = ''
|
||||
initError = ''
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
s9Host$: Observable<string>
|
||||
|
||||
AppStatus = AppStatus
|
||||
|
||||
server: PropertySubject<S9Server>
|
||||
currentServer: S9Server
|
||||
apps: PropertySubjectId<AppInstalledPreview>[] = []
|
||||
|
||||
subsToTearDown: Subscription[] = []
|
||||
|
||||
updatingFreeze = false
|
||||
updating = false
|
||||
segmentValue: 'services' | 'embassy' = 'services'
|
||||
|
||||
showCertDownload : boolean
|
||||
export class AppInstalledListPage {
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly syncDaemon: SyncDaemon,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
public readonly connectionService: ConnectionService,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subsToTearDown.forEach(s => s.unsubscribe())
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
this.server = this.serverModel.watch()
|
||||
this.apps = []
|
||||
this.cleanup(
|
||||
|
||||
// serverUpdateSubscription
|
||||
this.server.status.subscribe(status => {
|
||||
if (status === ServerStatus.UPDATING) {
|
||||
this.updating = true
|
||||
} else {
|
||||
if (!this.updatingFreeze) { this.updating = false }
|
||||
}
|
||||
}),
|
||||
|
||||
// newAppsSubscription
|
||||
this.appModel.watchDelta('add').subscribe(({ id }) => {
|
||||
if (this.apps.find(a => a.id === id)) return
|
||||
this.apps.push({ id, subject: this.appModel.watch(id) })
|
||||
},
|
||||
),
|
||||
|
||||
// appsDeletedSubscription
|
||||
this.appModel.watchDelta('delete').subscribe(({ id }) => {
|
||||
const i = this.apps.findIndex(a => a.id === id)
|
||||
this.apps.splice(i, 1)
|
||||
}),
|
||||
|
||||
// currentServerSubscription
|
||||
toObservable(this.server).subscribe(currentServerProperties => {
|
||||
this.currentServer = currentServerProperties
|
||||
}),
|
||||
)
|
||||
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.apps()).subscribe({
|
||||
next: apps => {
|
||||
this.apps = apps
|
||||
},
|
||||
error: e => {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async launchUiTab (id: string, event: Event) {
|
||||
launchUi (pkg: PackageDataEntry, event: Event): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const app = this.apps.find(app => app.id === id).subject
|
||||
|
||||
let uiAddress: string
|
||||
if (this.config.isTor()) {
|
||||
uiAddress = `http://${app.torAddress.getValue()}`
|
||||
} else {
|
||||
uiAddress = `https://${app.lanAddress.getValue()}`
|
||||
}
|
||||
return window.open(uiAddress, '_blank')
|
||||
window.open(this.config.launchableURL(pkg.installed), '_blank')
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await doForAtLeast([this.getServerAndApps()], 600)
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async getServerAndApps (): Promise<void> {
|
||||
try {
|
||||
await this.syncDaemon.sync()
|
||||
this.error = ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { DependencyListComponentModule } from 'src/app/components/dependency-list/dependency-list.component.module'
|
||||
import { AppInstalledShowPage } from './app-installed-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { AppBackupPageModule } from 'src/app/modals/app-backup/app-backup.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
|
||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -26,15 +21,12 @@ const routes: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
StatusComponentModule,
|
||||
DependencyListComponentModule,
|
||||
AppBackupPageModule,
|
||||
SharingModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
InstallWizardComponentModule,
|
||||
ErrorMessageComponentModule,
|
||||
InformationPopoverComponentModule,
|
||||
],
|
||||
declarations: [AppInstalledShowPage],
|
||||
|
||||
@@ -10,182 +10,173 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="{
|
||||
id: app.id | async,
|
||||
torAddress: app.torAddress | async,
|
||||
status: app.status | async,
|
||||
versionInstalled: app.versionInstalled | async,
|
||||
licenseName: app.licenseName | async,
|
||||
licenseLink: app.licenseLink | async,
|
||||
configuredRequirements: app.configuredRequirements | async,
|
||||
lastBackup: app.lastBackup | async,
|
||||
hasFetchedFull: app.hasFetchedFull | async,
|
||||
iconURL: app.iconURL | async,
|
||||
title: app.title | async,
|
||||
hasUI: app.hasUI | async,
|
||||
launchable: app.launchable | async,
|
||||
lanAddress: app.lanAddress | async
|
||||
} as vars" class="ion-padding-bottom">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<ion-content class="ion-padding-bottom">
|
||||
|
||||
<error-message [$error$]="$error$" [dismissable]="!!(app && app.id)"></error-message>
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<div class="top-plate" *ngIf="app && app.id">
|
||||
<ion-item class="no-cushion-item" lines="none">
|
||||
<ion-label class="ion-text-wrap" style="
|
||||
display: grid;
|
||||
grid-template-columns: 80px auto;
|
||||
margin: 0px;
|
||||
margin-top: 15px;"
|
||||
>
|
||||
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
|
||||
<img [src]="vars.iconURL | iconParse" />
|
||||
</ion-avatar>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="vars.title.length > 20">
|
||||
{{ vars.title }}
|
||||
</ion-text>
|
||||
<ion-text style="margin-top: -5px; margin-left: 2px;">
|
||||
{{ vars.versionInstalled | displayEmver }}
|
||||
</ion-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
|
||||
<ion-label class="status-readout">
|
||||
<status size="bold-large" [appStatus]="vars.status"></status>
|
||||
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
|
||||
Configure
|
||||
</ion-button>
|
||||
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" color="danger" (click)="stop()">
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
|
||||
Stop Backup
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
|
||||
Force Uninstall
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||
Fix
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" color="success" (click)="tryStart()">
|
||||
Start
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-button size="small" *ngIf="vars.hasUI" [disabled]="!vars.launchable" class="launch-button" expand="block" (click)="launchUiTab()">
|
||||
Launch Web Interface
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="app && app.id && vars.status !== 'INSTALLING'">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<!-- addresses -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>{{ vars.torAddress }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p *ngIf="!hideLAN">{{ vars.lanAddress }}</p>
|
||||
<p *ngIf="hideLAN"><ion-text color="warning">No LAN address for {{ vars.title }} {{ vars.versionInstalled }}</ion-text></p>
|
||||
</ion-label>
|
||||
<ion-button *ngIf="!hideLAN" slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<!-- backups -->
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<!-- create backup -->
|
||||
<ion-item button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('create')">
|
||||
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
|
||||
<ion-label style="display: flex; flex-direction: column;">
|
||||
<ion-text color="primary">Create Backup</ion-text>
|
||||
<ion-text color="medium" style="font-size: x-small">
|
||||
Last Backup: {{vars.lastBackup ? (vars.lastBackup | date: 'short') : 'never'}}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- restore backup -->
|
||||
<ion-item lines="none" button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('restore')">
|
||||
<ion-icon slot="start" name="color-wand-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<!-- instructions -->
|
||||
<ion-item [routerLink]="['instructions']">
|
||||
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Instructions</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- config -->
|
||||
<ion-item [disabled]="[AppStatus.CREATING_BACKUP, AppStatus.RESTORING_BACKUP, AppStatus.INSTALLING, AppStatus.DEAD] | includes: vars.status" [routerLink]="['config']">
|
||||
<ion-icon slot="start" name="construct-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- metrics -->
|
||||
<ion-item [routerLink]="['metrics']">
|
||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- actions -->
|
||||
<ion-item [routerLink]="['actions']">
|
||||
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- logs -->
|
||||
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- marketplace -->
|
||||
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
|
||||
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- license -->
|
||||
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">License</ion-text></ion-label>
|
||||
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<!-- dependencies -->
|
||||
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">
|
||||
<ion-item-divider [id]="'service-requirements-' + vars.id">Dependencies
|
||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(dependencyDefintion(), $event)">
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<dependency-list [$loading$]="$loadingDependencies$" depType="installed" [hostApp]="app | peekProperties" [dependencies]="vars.configuredRequirements"></dependency-list>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
|
||||
<ng-container *ngIf="vars.status !== AppStatus.INSTALLING && vars.status !== 'CREATING_BACKUP'">
|
||||
<!-- uninstall -->
|
||||
<ion-item style="--background: transparent" button (click)="uninstall()">
|
||||
<ion-icon slot="start" name="trash-outline" color="medium"></ion-icon>
|
||||
<ion-label><ion-text color="danger">Uninstall</ion-text></ion-label>
|
||||
<ng-container *ngrxLet="connectionService.monitor$() as connection">
|
||||
<ng-container *ngIf="pkg | manifest as manifest">
|
||||
<ng-container *ngIf="pkg | status : connection as status">
|
||||
<div class="top-plate">
|
||||
<ion-item class="no-cushion-item" lines="none">
|
||||
<ion-label class="ion-text-wrap" style="
|
||||
display: grid;
|
||||
grid-template-columns: 80px auto;
|
||||
margin: 0px;
|
||||
margin-top: 15px;"
|
||||
>
|
||||
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
|
||||
<img [src]="pkg['static-files'].icon" />
|
||||
</ion-avatar>
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="manifest.title.length > 20">
|
||||
{{ manifest.title }}
|
||||
</ion-text>
|
||||
<ion-text style="margin-top: -5px; margin-left: 2px;">
|
||||
{{ manifest.version | displayEmver }}
|
||||
</ion-text>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
|
||||
<ion-label class="status-readout">
|
||||
<status size="bold-large" [pkg]="pkg" [connection]="connection"></status>
|
||||
<ion-button *ngIf="status === FeStatus.NeedsConfig" expand="block" fill="outline" [routerLink]="['config']">
|
||||
Configure
|
||||
</ion-button>
|
||||
<ion-button *ngIf="status === FeStatus.Running" expand="block" fill="outline" color="danger" (click)="stop()">
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button *ngIf="status === FeStatus.DependencyIssue" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||
Fix
|
||||
</ion-button>
|
||||
<ion-button *ngIf="status === FeStatus.Stopped" expand="block" fill="outline" color="success" (click)="tryStart()">
|
||||
Start
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
|
||||
Launch Web Interface
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : status)">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<!-- interfaces -->
|
||||
<ion-item [routerLink]="['interfaces']">
|
||||
<ion-icon slot="start" name="aperture-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Interfaces</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- instructions -->
|
||||
<ion-item [routerLink]="['instructions']">
|
||||
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Instructions</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- config -->
|
||||
<ion-item [disabled]="[FeStatus.Installing, FeStatus.Updating, FeStatus.Removing, FeStatus.BackingUp, FeStatus.Restoring] | includes : status" [routerLink]="['config']">
|
||||
<ion-icon slot="start" name="construct-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- properties -->
|
||||
<ion-item [routerLink]="['properties']">
|
||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- actions -->
|
||||
<ion-item [routerLink]="['actions']">
|
||||
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- logs -->
|
||||
<ion-item [routerLink]="['logs']">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- restore -->
|
||||
<ion-item button [disabled]="[FeStatus.Connecting, FeStatus.Installing, FeStatus.Updating, FeStatus.Stopping, FeStatus.Removing, FeStatus.BackingUp, FeStatus.Restoring] | includes : status" [routerLink]="['restore']">
|
||||
<ion-icon slot="start" name="color-wand-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- donate -->
|
||||
<ion-item button [href]="manifest['donation-url']" target="_blank">
|
||||
<ion-icon slot="start" name="shapes-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Donate</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- marketplace -->
|
||||
<ion-item [routerLink]="['/services', 'marketplace', manifest.id]">
|
||||
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- dependencies -->
|
||||
<ng-container *ngIf="!(manifest.dependencies | empty)">
|
||||
<ion-item-divider id="dependencies">
|
||||
Dependencies
|
||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(depDefinition, $event)">
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
|
||||
<div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue">
|
||||
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item" lines="none">
|
||||
<ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;">
|
||||
<div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div>
|
||||
<img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" />
|
||||
</ion-avatar>
|
||||
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
|
||||
<h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4>
|
||||
<p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
|
||||
<p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
|
||||
</ion-label>
|
||||
|
||||
<ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" [routerLink]="['/services', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
|
||||
View
|
||||
</ion-button>
|
||||
|
||||
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
|
||||
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small">
|
||||
Install
|
||||
</ion-button>
|
||||
|
||||
<ng-container *ngIf="localDep && localDep.state === PackageState.Installed">
|
||||
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
|
||||
Start
|
||||
</ion-button>
|
||||
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)" color="primary" fill="outline" style="font-size: x-small">
|
||||
Update
|
||||
</ion-button>
|
||||
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" (click)="fixDep('configure', dep.key)" color="primary" fill="outline" style="font-size: x-small">
|
||||
Configure
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner">
|
||||
<ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider></ion-item-divider>
|
||||
|
||||
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.BackingUp, FeStatus.Restoring] | includes : status)">
|
||||
<!-- uninstall -->
|
||||
<ion-item button (click)="uninstall()">
|
||||
<ion-icon slot="start" name="trash-outline" color="danger"></ion-icon>
|
||||
<ion-label><ion-text color="danger">Uninstall</ion-text></ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
|
||||
@@ -48,3 +48,19 @@
|
||||
--border-radius: 10px;
|
||||
margin: 12px 10px;
|
||||
}
|
||||
|
||||
.dep-badge {
|
||||
position: absolute; width: 2.5vh;
|
||||
height: 2.5vh;
|
||||
border-radius: 50px;
|
||||
left: -1vh;
|
||||
top: -1vh;
|
||||
}
|
||||
|
||||
.dep-issue {
|
||||
background: radial-gradient(var(--ion-color-warning) 40%, transparent)
|
||||
}
|
||||
|
||||
.dep-sat {
|
||||
background: radial-gradient(var(--ion-color-success) 40%, transparent)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { AlertController, NavController, ToastController, ModalController, IonContent, PopoverController } from '@ionic/angular'
|
||||
import { AlertController, NavController, ModalController, IonContent, PopoverController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { chill, pauseFor } from 'src/app/util/misc.util'
|
||||
import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util'
|
||||
import { AppBackupPage } from 'src/app/modals/app-backup/app-backup.page'
|
||||
import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { chill } from 'src/app/util/misc.util'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { Observable, of, Subscription } from 'rxjs'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, PackageDataEntry, PackageState } from 'src/app/models/patch-db/data-model'
|
||||
import { FEStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-installed-show',
|
||||
templateUrl: './app-installed-show.page.html',
|
||||
styleUrls: ['./app-installed-show.page.scss'],
|
||||
})
|
||||
export class AppInstalledShowPage extends Cleanup {
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
$loadingDependencies$ = new BehaviorSubject(false) // when true, dependencies will render with spinners.
|
||||
|
||||
$error$ = new BehaviorSubject<string>('')
|
||||
app: PropertySubject<AppInstalledFull> = { } as any
|
||||
appId: string
|
||||
AppStatus = AppStatus
|
||||
showInstructions = false
|
||||
|
||||
export class AppInstalledShowPage {
|
||||
error: string
|
||||
pkgId: string
|
||||
pkg: PackageDataEntry
|
||||
pkgSub: Subscription
|
||||
hideLAN: boolean
|
||||
|
||||
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
|
||||
FeStatus = FEStatus
|
||||
PackageState = PackageState
|
||||
DependencyErrorType = DependencyErrorType
|
||||
|
||||
depDefinition = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
@@ -44,115 +40,44 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly popoverController: PopoverController,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
public readonly patch: PatchDbModel,
|
||||
public readonly connectionService: ConnectionService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
||||
|
||||
this.cleanup(
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
|
||||
.pipe(
|
||||
tap(app => {
|
||||
this.app = app
|
||||
const appP = peekProperties(this.app)
|
||||
this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0
|
||||
}),
|
||||
concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack
|
||||
catchError(e => of(this.setError(e))),
|
||||
).subscribe(),
|
||||
)
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.pkgSub = this.patch.watch$('package-data', this.pkgId).subscribe(pkg => this.pkg = pkg)
|
||||
}
|
||||
|
||||
ionViewDidEnter () {
|
||||
markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())
|
||||
async ngOnDestroy () {
|
||||
this.pkgSub.unsubscribe()
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.getApp(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async scrollToRequirements () {
|
||||
return this.scrollToElement('service-requirements-' + this.appId)
|
||||
}
|
||||
|
||||
async getApp (): Promise<void> {
|
||||
try {
|
||||
await this.preload.loadInstalledApp(this.appId)
|
||||
this.clearError()
|
||||
} catch (e) {
|
||||
this.setError(e)
|
||||
}
|
||||
}
|
||||
|
||||
async launchUiTab () {
|
||||
let uiAddress: string
|
||||
if (this.config.isTor()) {
|
||||
uiAddress = `http://${this.app.torAddress.getValue()}`
|
||||
} else {
|
||||
uiAddress = `https://${this.app.lanAddress.getValue()}`
|
||||
}
|
||||
return window.open(uiAddress, '_blank')
|
||||
}
|
||||
|
||||
async copyTor () {
|
||||
const app = peekProperties(this.app)
|
||||
let message = ''
|
||||
await copyToClipboard(app.torAddress || '').then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async copyLAN () {
|
||||
const app = peekProperties(this.app)
|
||||
let message = ''
|
||||
await copyToClipboard(app.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
launchUiTab (): void {
|
||||
window.open(this.config.launchableURL(this.pkg.installed), '_blank')
|
||||
}
|
||||
|
||||
async stop (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
const { id, title, version } = this.pkg.installed.manifest
|
||||
await this.loader.of({
|
||||
message: `Stopping ${app.title}...`,
|
||||
message: `Stopping...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync(async () => {
|
||||
const { breakages } = await this.apiService.stopApp(this.appId, true)
|
||||
const { breakages } = await this.apiService.dryStopPackage({ id })
|
||||
|
||||
if (breakages.length) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.stop({
|
||||
id: app.id,
|
||||
title: app.title,
|
||||
version: app.versionInstalled,
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
breakages,
|
||||
}),
|
||||
)
|
||||
@@ -160,76 +85,28 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
if (cancelled) return { }
|
||||
}
|
||||
|
||||
return this.apiService.stopApp(this.appId).then(chill)
|
||||
return this.apiService.stopPackage({ id }).then(chill)
|
||||
}).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
async tryStart (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
if (app.startAlert) {
|
||||
this.presentAlertStart(app)
|
||||
const message = this.pkg.installed.manifest.alerts.start
|
||||
if (message) {
|
||||
this.presentAlertStart(message)
|
||||
} else {
|
||||
this.start(app)
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalBackup (type: 'create' | 'restore') {
|
||||
const modal = await this.modalCtrl.create({
|
||||
backdropDismiss: false,
|
||||
component: AppBackupPage,
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
componentProps: {
|
||||
app: peekProperties(this.app),
|
||||
type,
|
||||
},
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAlertStopBackup (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Warning',
|
||||
message: `${app.title} is not finished backing up. Are you sure you want stop the process?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Stop',
|
||||
cssClass: 'alert-danger',
|
||||
handler: () => {
|
||||
this.stopBackup()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async stopBackup (): Promise<void> {
|
||||
await this.loader.of({
|
||||
message: `Stopping backup...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(this.apiService.stopAppBackup(this.appId))
|
||||
.catch (e => this.setError(e))
|
||||
}
|
||||
|
||||
async uninstall () {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
const { id, title, version, alerts } = this.pkg.installed.manifest
|
||||
const data = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.uninstall({
|
||||
id: app.id,
|
||||
title: app.title,
|
||||
version: app.versionInstalled,
|
||||
uninstallAlert: app.uninstallAlert,
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
uninstallAlert: alerts.uninstall,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -251,10 +128,64 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
return await popover.present()
|
||||
}
|
||||
|
||||
private async presentAlertStart (app: AppInstalledFull): Promise<void> {
|
||||
scrollToRequirements () {
|
||||
const el = document.getElementById('dependencies')
|
||||
if (!el) return
|
||||
let y = el.offsetTop
|
||||
return this.content.scrollToPoint(0, y, 1000)
|
||||
}
|
||||
|
||||
async fixDep (action: 'install' | 'update' | 'configure', id: string): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(id)
|
||||
case 'configure':
|
||||
return this.configureDep(id)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep (depId: string): Promise<void> {
|
||||
const version = this.pkg.installed.manifest.dependencies[depId].version
|
||||
const dependentTitle = this.pkg.installed.manifest.title
|
||||
|
||||
const installRec: Recommendation = {
|
||||
dependentId: this.pkgId,
|
||||
dependentTitle,
|
||||
dependentIcon: this.pkg['static-files'].icon,
|
||||
version,
|
||||
description: `${dependentTitle} requires an install of ${(this.pkg.installed.status['dependency-errors'][depId] as DependencyErrorNotInstalled)?.title} satisfying ${version}.`,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { installRec },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(`/services/marketplace/${depId}`, navigationExtras)
|
||||
}
|
||||
|
||||
private async configureDep (depId: string): Promise<void> {
|
||||
const configErrors = (this.pkg.installed.status['dependency-errors'][depId] as DependencyErrorConfigUnsatisfied).errors
|
||||
|
||||
const description = `<ul>${configErrors.map(d => `<li>${d}</li>`).join('\n')}</ul>`
|
||||
const dependentTitle = this.pkg.installed.manifest.title
|
||||
|
||||
const configRecommendation: Recommendation = {
|
||||
dependentId: this.pkgId,
|
||||
dependentTitle,
|
||||
dependentIcon: this.pkg['static-files'].icon,
|
||||
description,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { configRecommendation },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(`/services/installed/${depId}/config`, navigationExtras)
|
||||
}
|
||||
|
||||
private async presentAlertStart (message: string): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: app.startAlert,
|
||||
message,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
@@ -263,7 +194,7 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
{
|
||||
text: 'Start',
|
||||
handler: () => {
|
||||
this.start(app)
|
||||
this.start()
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -271,40 +202,18 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async start (app: AppInstalledFull): Promise<void> {
|
||||
private async start (): Promise<void> {
|
||||
this.loader.of({
|
||||
message: `Starting ${app.title}...`,
|
||||
message: `Starting...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.startApp(this.appId),
|
||||
this.apiService.startPackage({ id: this.pkgId }),
|
||||
).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
private setError (e: Error): Observable<void> {
|
||||
this.$error$.next(e.message)
|
||||
this.error = e.message
|
||||
return of()
|
||||
}
|
||||
|
||||
private clearError () {
|
||||
this.$error$.next('')
|
||||
}
|
||||
|
||||
private async scrollToElement (elementId: string) {
|
||||
const el = document.getElementById(elementId)
|
||||
|
||||
if (!el) return
|
||||
|
||||
let y = el.offsetTop
|
||||
return this.content.scrollToPoint(0, y, 1000)
|
||||
}
|
||||
|
||||
private syncWhenDependencyInstalls (): Observable<void> {
|
||||
return this.app.configuredRequirements.pipe(
|
||||
filter(deps => !!deps),
|
||||
switchMap(reqs => this.appModel.watchForInstallations(reqs)),
|
||||
concatMap(() => markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())),
|
||||
catchError(e => of(console.error(e))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const routes: Routes = [
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppInstructionsPage],
|
||||
declarations: [
|
||||
AppInstructionsPage,
|
||||
],
|
||||
})
|
||||
export class AppInstructionsPageModule { }
|
||||
|
||||
@@ -7,22 +7,14 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding">
|
||||
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
|
||||
<ion-item *ngIf="!app.instructions">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No instructions for {{ app.title }} {{ app.versionInstalled }}.</p>
|
||||
</ion-label>
|
||||
<ng-template #loaded>
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<div style="
|
||||
padding-left: var(--ion-padding,16px);
|
||||
padding-right: var(--ion-padding,16px);
|
||||
padding-bottom: var(--ion-padding,16px);
|
||||
" *ngIf="app.instructions" [innerHTML]="app.instructions | markdown"></div>
|
||||
</ng-container>
|
||||
<div *ngIf="instructions" class="instuctions-padding" [innerHTML]="instructions | markdown"></div>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,3 @@
|
||||
.instructions-padding {
|
||||
padding: 0 16px 16px 16px
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { peekProperties } from 'src/app/util/property-subject.util'
|
||||
import { concatMap, take, tap } from 'rxjs/operators'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { Method } from 'src/app/services/http.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-instructions',
|
||||
@@ -12,25 +11,34 @@ import { peekProperties } from 'src/app/util/property-subject.util'
|
||||
styleUrls: ['./app-instructions.page.scss'],
|
||||
})
|
||||
export class AppInstructionsPage {
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
instructions: string
|
||||
loading = true
|
||||
error = ''
|
||||
app: AppInstalledFull = { } as any
|
||||
appId: string
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
||||
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).subscribe({
|
||||
next: app => this.app = peekProperties(app),
|
||||
error: e => {
|
||||
console.error(e)
|
||||
const pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.patch.watch$('package-data', pkgId)
|
||||
.pipe(
|
||||
concatMap(pkg => this.apiService.getStatic(pkg['static-files'].instructions)),
|
||||
tap(instructions => {
|
||||
console.log(instructions)
|
||||
this.instructions = instructions
|
||||
}),
|
||||
take(1),
|
||||
)
|
||||
.subscribe(
|
||||
() => { this.loading = false },
|
||||
e => {
|
||||
this.error = e.message
|
||||
this.loading = false
|
||||
},
|
||||
})
|
||||
() => console.log('COMPLETE'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppInterfacesPage } from './app-interfaces.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppInterfacesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppInterfacesPage],
|
||||
})
|
||||
export class AppInterfacesPageModule { }
|
||||
@@ -0,0 +1,52 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Interfaces</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ng-container *ngIf="patch.watch$('package-data', pkgId) | ngrxPush as pkg">
|
||||
|
||||
<ion-card style="margin-bottom: 16px;" *ngFor="let interface of pkg.installed.manifest.interfaces | keyvalue: asIsOrder">
|
||||
<ion-card-header>
|
||||
<ion-card-title>
|
||||
{{ interface.value.name }}
|
||||
<ion-button class="vertical-align" *ngIf="interface.value.ui" [disabled]="!(pkg | isLaunchable)" fill="clear" (click)="launch(pkg.installed)">
|
||||
<ion-icon slot="icon-only" name="rocket-outline" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-card-title>
|
||||
<ion-card-subtitle>{{ interface.value.description }}</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ng-container *ngIf="pkg.installed['interface-info'].addresses[interface.key] as int">
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>{{ 'http://' + int['tor-address'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy('http://' + int['tor-address'])">
|
||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p>{{ 'https://' + int['lan-address'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copy('https://' + int['lan-address'])">
|
||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item button detail="true">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Advanced
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,4 @@
|
||||
.vertical-align {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
|
||||
@Component({
|
||||
selector: 'app-Interfaces',
|
||||
templateUrl: './app-Interfaces.page.html',
|
||||
styleUrls: ['./app-Interfaces.page.scss'],
|
||||
})
|
||||
export class AppInterfacesPage {
|
||||
pkgId: string
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
}
|
||||
|
||||
async copy (address: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
.then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
launch (installed: InstalledPackageDataEntry): void {
|
||||
window.open(this.config.launchableURL(installed), '_blank')
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,12 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding" color="light">
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ion-spinner *ngIf="!logs" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<p style="white-space: pre-line;">{{ logs }}</p>
|
||||
</ion-content>
|
||||
@@ -2,9 +2,6 @@ import { Component, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'app-logs',
|
||||
@@ -13,38 +10,29 @@ import { BehaviorSubject } from 'rxjs'
|
||||
})
|
||||
export class AppLogsPage {
|
||||
@ViewChild(IonContent, { static: false }) private content: IonContent
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
pkgId: string
|
||||
logs = ''
|
||||
error = ''
|
||||
appId: string
|
||||
logs: string
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
||||
|
||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
||||
this.getLogs(),
|
||||
pauseFor(600),
|
||||
]))
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.getLogs()
|
||||
}
|
||||
|
||||
async getLogs () {
|
||||
this.logs = ''
|
||||
this.$loading$.next(true)
|
||||
|
||||
try {
|
||||
const logs = await this.apiService.getAppLogs(this.appId)
|
||||
this.logs = logs.join('\n\n')
|
||||
this.error = ''
|
||||
const logs = await this.apiService.getPackageLogs({ id: this.pkgId })
|
||||
this.logs = logs.map(l => `${l.timestamp} ${l.log}`).join('\n\n')
|
||||
setTimeout(async () => await this.content.scrollToBottom(100), 200)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.$loading$.next(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Properties</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- not running -->
|
||||
<ion-item *ngIf="app.status !== 'RUNNING'" class="ion-margin-bottom">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p><ion-text color="warning">{{ app.title }} is not running. Information on this page could be innacurate.</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no metrics -->
|
||||
<ion-item *ngIf="($hasMetrics$ | async) === false">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No properties for {{ app.title }} {{ app.versionInstalled }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- metrics -->
|
||||
<ion-item-group *ngIf="($hasMetrics$ | async) === true">
|
||||
<div *ngFor="let keyval of $metrics$ | async | keyvalue: asIsOrder">
|
||||
<!-- object -->
|
||||
<ion-item button detail="false" *ngIf="keyval.value.type === 'object'" (click)="goToNested(keyval.key)">
|
||||
<ion-button *ngIf="keyval.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(keyval, $event)">
|
||||
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ keyval.key }}</h2>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="arrow-forward"></ion-icon>
|
||||
</ion-item>
|
||||
<!-- not object -->
|
||||
<ion-item *ngIf="keyval.value.type === 'string'">
|
||||
<ion-button *ngIf="keyval.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(keyval, $event)">
|
||||
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ keyval.key }}</h2>
|
||||
<p>{{ keyval.value.masked && !unmasked[keyval.key] ? (keyval.value.value | mask ) : (keyval.value.value | truncateEnd : 100) }}</p>
|
||||
</ion-label>
|
||||
<div slot="end" *ngIf="keyval.value.copyable || keyval.value.qr">
|
||||
<ion-button *ngIf="keyval.value.masked" fill="clear" (click)="toggleMask(keyval.key)">
|
||||
<ion-icon slot="icon-only" [name]="unmasked[keyval.key] ? 'eye-off-outline' : 'eye-outline'" [color]="unmasked[keyval.key] ? 'danger' : 'primary'" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="keyval.value.qr" fill="clear" (click)="showQR(keyval.value.value)">
|
||||
<ion-icon slot="icon-only" name="qr-code-outline" size="small" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="keyval.value.copyable" fill="clear" (click)="copy(keyval.value.value)">
|
||||
<ion-icon slot="icon-only" name="copy-outline" size="small" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
@@ -1,138 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { AlertController, NavController, PopoverController, ToastController } from '@ionic/angular'
|
||||
import { AppMetrics } from 'src/app/util/metrics.util'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { AppMetricStore } from './metric-store'
|
||||
import * as JSONpointer from 'json-pointer'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { peekProperties } from 'src/app/util/property-subject.util'
|
||||
|
||||
@Component({
|
||||
selector: 'app-metrics',
|
||||
templateUrl: './app-metrics.page.html',
|
||||
styleUrls: ['./app-metrics.page.scss'],
|
||||
})
|
||||
export class AppMetricsPage {
|
||||
error = ''
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
appId: string
|
||||
pointer: string
|
||||
qrCode: string
|
||||
app: AppInstalledFull
|
||||
$metrics$ = new BehaviorSubject<AppMetrics>({ })
|
||||
$hasMetrics$ = new BehaviorSubject<boolean>(null)
|
||||
unmasked: { [key: string]: boolean } = { }
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly popoverCtrl: PopoverController,
|
||||
private readonly metricStore: AppMetricStore,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly preload: ModelPreload,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId')
|
||||
this.pointer = this.route.queryParams['pointer']
|
||||
|
||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
||||
this.preload.appFull(this.appId).toPromise(),
|
||||
this.getMetrics(),
|
||||
pauseFor(600),
|
||||
])).then(([app]) => {
|
||||
this.app = peekProperties(app)
|
||||
this.metricStore.watch().subscribe(m => {
|
||||
const metrics = JSONpointer.get(m, this.pointer || '')
|
||||
this.$metrics$.next(metrics)
|
||||
})
|
||||
this.$metrics$.subscribe(m => {
|
||||
this.$hasMetrics$.next(!!Object.keys(m || { }).length)
|
||||
})
|
||||
this.route.queryParams.subscribe(queryParams => {
|
||||
if (queryParams['pointer'] === this.pointer) return
|
||||
this.pointer = queryParams['pointer']
|
||||
const metrics = JSONpointer.get(this.metricStore.$metrics$.getValue(), this.pointer || '')
|
||||
this.$metrics$.next(metrics)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await Promise.all([
|
||||
this.getMetrics(),
|
||||
pauseFor(600),
|
||||
])
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async getMetrics (): Promise<void> {
|
||||
try {
|
||||
const metrics = await this.apiService.getAppMetrics(this.appId)
|
||||
this.metricStore.update(metrics)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async presentDescription (metric: { key: string, value: AppMetrics[''] }, e: Event) {
|
||||
e.stopPropagation()
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: metric.key,
|
||||
message: metric.value.description,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async goToNested (key: string): Promise<any> {
|
||||
this.navCtrl.navigateForward(`/services/installed/${this.appId}/metrics`, {
|
||||
queryParams: {
|
||||
pointer: `${this.pointer || ''}/${key}/value`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async copy (text: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(text).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async showQR (text: string, ev: any): Promise<void> {
|
||||
const popover = await this.popoverCtrl.create({
|
||||
component: QRComponent,
|
||||
cssClass: 'qr-popover',
|
||||
event: ev,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
return await popover.present()
|
||||
}
|
||||
|
||||
toggleMask (key: string) {
|
||||
this.unmasked[key] = !this.unmasked[key]
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { AppMetrics } from '../../../util/metrics.util'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AppMetricStore {
|
||||
$metrics$: BehaviorSubject<AppMetrics> = new BehaviorSubject({ })
|
||||
watch () { return this.$metrics$.asObservable() }
|
||||
|
||||
update (metrics: AppMetrics): void {
|
||||
this.$metrics$.next(metrics)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppMetricsPage } from './app-metrics.page'
|
||||
import { AppPropertiesPage } from './app-properties.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
@@ -10,7 +10,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppMetricsPage,
|
||||
component: AppPropertiesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -23,6 +23,6 @@ const routes: Routes = [
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppMetricsPage],
|
||||
declarations: [AppPropertiesPage],
|
||||
})
|
||||
export class AppMetricsPageModule { }
|
||||
export class AppPropertiesPageModule { }
|
||||
@@ -0,0 +1,74 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Properties</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<ng-container *ngIf="patch.watch$('package-data', pkgId) | ngrxPush as pkg">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- not running -->
|
||||
<ion-item *ngIf="pkg.installed.status.main.status !== FeStatus.Running" class="ion-margin-bottom">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p><ion-text color="warning">Service not running. Information on this page could be inaccurate.</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no properties -->
|
||||
<ion-item *ngIf="(hasProperties$ | ngrxPush) === false">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No properties.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- properties -->
|
||||
<ion-item-group *ngIf="(hasProperties$ | ngrxPush) === true">
|
||||
<div *ngFor="let prop of properties$ | ngrxPush | keyvalue: asIsOrder">
|
||||
<!-- object -->
|
||||
<ion-item button detail="true" *ngIf="prop.value.type === 'object'" (click)="goToNested(prop.key)">
|
||||
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
|
||||
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ prop.key }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- not object -->
|
||||
<ion-item *ngIf="prop.value.type === 'string'">
|
||||
<ion-button *ngIf="prop.value.description" class="help-button" fill="clear" slot="start" (click)="presentDescription(prop, $event)">
|
||||
<ion-icon size="small" slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ prop.key }}</h2>
|
||||
<p>{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | mask ) : (prop.value.value | truncateEnd : 100) }}</p>
|
||||
</ion-label>
|
||||
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
|
||||
<ion-button *ngIf="prop.value.masked" fill="clear" (click)="toggleMask(prop.key)">
|
||||
<ion-icon slot="icon-only" [name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'" [color]="unmasked[prop.key] ? 'danger' : 'primary'" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="prop.value.qr" fill="clear" (click)="showQR(prop.value.value)">
|
||||
<ion-icon slot="icon-only" name="qr-code-outline" size="small" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="prop.value.copyable" fill="clear" (click)="copy(prop.value.value)">
|
||||
<ion-icon slot="icon-only" name="copy-outline" size="small" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { isEmptyObject, pauseFor } from 'src/app/util/misc.util'
|
||||
import { BehaviorSubject, Subject } from 'rxjs'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { AlertController, NavController, PopoverController, ToastController } from '@ionic/angular'
|
||||
import { PackageProperties } from 'src/app/util/properties.util'
|
||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||
import { PropertyStore } from './property-store'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
import * as JSONpointer from 'json-pointer'
|
||||
import { FEStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-properties',
|
||||
templateUrl: './app-properties.page.html',
|
||||
styleUrls: ['./app-properties.page.scss'],
|
||||
})
|
||||
export class AppPropertiesPage {
|
||||
error = ''
|
||||
loading = true
|
||||
pkgId: string
|
||||
pointer: string
|
||||
qrCode: string
|
||||
properties$ = new BehaviorSubject<PackageProperties>({ })
|
||||
hasProperties$ = new BehaviorSubject<boolean>(null)
|
||||
unmasked: { [key: string]: boolean } = { }
|
||||
FeStatus = FEStatus
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly popoverCtrl: PopoverController,
|
||||
private readonly propertyStore: PropertyStore,
|
||||
private readonly navCtrl: NavController,
|
||||
public patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.pointer = this.route.queryParams['pointer']
|
||||
|
||||
this.getProperties().then(() => this.loading = false)
|
||||
|
||||
this.propertyStore.watch$().subscribe(m => {
|
||||
const properties = JSONpointer.get(m, this.pointer || '')
|
||||
this.properties$.next(properties)
|
||||
})
|
||||
this.properties$.subscribe(m => {
|
||||
this.hasProperties$.next(!isEmptyObject(m))
|
||||
})
|
||||
this.route.queryParams.subscribe(queryParams => {
|
||||
if (queryParams['pointer'] === this.pointer) return
|
||||
this.pointer = queryParams['pointer']
|
||||
const properties = JSONpointer.get(this.propertyStore.properties$.getValue(), this.pointer || '')
|
||||
this.properties$.next(properties)
|
||||
})
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
await this.getProperties(),
|
||||
event.target.complete()
|
||||
}
|
||||
|
||||
async presentDescription (property: { key: string, value: PackageProperties[''] }, e: Event) {
|
||||
e.stopPropagation()
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: property.key,
|
||||
message: property.value.description,
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async goToNested (key: string): Promise<any> {
|
||||
this.navCtrl.navigateForward(`/services/installed/${this.pkgId}/properties`, {
|
||||
queryParams: {
|
||||
pointer: `${this.pointer || ''}/${key}/value`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async copy (text: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(text).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async showQR (text: string, ev: any): Promise<void> {
|
||||
const popover = await this.popoverCtrl.create({
|
||||
component: QRComponent,
|
||||
cssClass: 'qr-popover',
|
||||
event: ev,
|
||||
componentProps: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
return await popover.present()
|
||||
}
|
||||
|
||||
toggleMask (key: string) {
|
||||
this.unmasked[key] = !this.unmasked[key]
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
|
||||
private async getProperties (): Promise<void> {
|
||||
try {
|
||||
const properties = await this.apiService.getPackageProperties({ id: this.pkgId })
|
||||
this.propertyStore.update(properties)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { PackageProperties } from '../../../util/properties.util'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PropertyStore {
|
||||
properties$: BehaviorSubject<PackageProperties> = new BehaviorSubject({ })
|
||||
watch$ () { return this.properties$.asObservable() }
|
||||
|
||||
update (properties: PackageProperties): void {
|
||||
this.properties$.next(properties)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppRestorePage } from './app-restore.page'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BackupConfirmationComponentModule } from 'src/app/modals/backup-confirmation/backup-confirmation.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppRestorePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
RouterModule.forChild(routes),
|
||||
BackupConfirmationComponentModule,
|
||||
PwaBackComponentModule,
|
||||
],
|
||||
declarations: [
|
||||
AppRestorePage,
|
||||
],
|
||||
})
|
||||
export class AppRestorePageModule { }
|
||||
@@ -0,0 +1,64 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Restore From Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="doRefresh()" color="primary">
|
||||
<ion-icon slot="icon-only" name="reload-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top" *ngIf="patch.watch$('package-data', pkgId) | ngrxPush as pkg">
|
||||
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p><ion-text color="dark">About</ion-text></p>
|
||||
<p>
|
||||
Select a location from which to restore {{ pkg.installed.manifest.title }}. This will overwrite all current data.
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-spinner *ngIf="loading; else loaded" name="lines" color="warning" class="center"></ion-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
|
||||
<ion-item *ngIf="allPartitionsMounted">
|
||||
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you intend to restore.</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ion-card *ngFor="let disk of disks | keyvalue">
|
||||
<ion-card-header>
|
||||
<ion-card-title>
|
||||
{{ disk.value.size }}
|
||||
</ion-card-title>
|
||||
<ion-card-subtitle>
|
||||
{{ disk.key }}
|
||||
</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-item-group>
|
||||
<ion-item button *ngFor="let partition of disk.value.partitions | keyvalue" [disabled]="partition.value['is-mounted']" (click)="presentModal(partition.key, partition.value)">
|
||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ partition.value.label || partition.key }} ({{ partition.value.size || 'unknown size' }})</h2>
|
||||
<p *ngIf="!partition.value['is-mounted']; else unavailable"><ion-text color="success">Available</ion-text></p>
|
||||
<ng-template #unavailable>
|
||||
<p><ion-text color="danger">Unavailable</ion-text></p>
|
||||
</ng-template>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ng-template>
|
||||
|
||||
</ion-content>
|
||||
91
ui/src/app/pages/apps-routes/app-restore/app-restore.page.ts
Normal file
91
ui/src/app/pages/apps-routes/app-restore/app-restore.page.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { BackupConfirmationComponent } from 'src/app/modals/backup-confirmation/backup-confirmation.component'
|
||||
import { DiskInfo, PartitionInfoEntry } from 'src/app/services/api/api-types'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-restore',
|
||||
templateUrl: './app-restore.page.html',
|
||||
styleUrls: ['./app-restore.page.scss'],
|
||||
})
|
||||
export class AppRestorePage {
|
||||
disks: DiskInfo
|
||||
pkgId: string
|
||||
loading = true
|
||||
error: string
|
||||
allPartitionsMounted: boolean
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
public readonly patch: PatchDbModel,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
this.getExternalDisks()
|
||||
}
|
||||
|
||||
async doRefresh () {
|
||||
this.loading = true
|
||||
await this.getExternalDisks()
|
||||
}
|
||||
|
||||
async getExternalDisks (): Promise<void> {
|
||||
try {
|
||||
this.disks = await this.apiService.getDisks({ })
|
||||
this.allPartitionsMounted = Object.values(this.disks).every(d => Object.values(d.partitions).every(p => p['is-mounted']))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async presentModal (logicalname: string, partition: PartitionInfoEntry): Promise<void> {
|
||||
const m = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
name: partition.label || logicalname,
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
component: BackupConfirmationComponent,
|
||||
backdropDismiss: false,
|
||||
})
|
||||
|
||||
m.onWillDismiss().then(res => {
|
||||
const data = res.data
|
||||
if (data.cancel) return
|
||||
this.restore(logicalname, data.password)
|
||||
})
|
||||
|
||||
return await m.present()
|
||||
}
|
||||
|
||||
private async restore (logicalname: string, password: string): Promise<void> {
|
||||
this.error = ''
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.restorePackage({
|
||||
id: this.pkgId,
|
||||
logicalname,
|
||||
password,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error = e.message
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,45 +7,53 @@ const routes: Routes = [
|
||||
redirectTo: 'installed',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'marketplace',
|
||||
loadChildren: () => import('./app-available-list/app-available-list.module').then(m => m.AppAvailableListPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed',
|
||||
loadChildren: () => import('./app-installed-list/app-installed-list.module').then(m => m.AppInstalledListPageModule),
|
||||
},
|
||||
{
|
||||
path: 'marketplace/:appId',
|
||||
loadChildren: () => import('./app-available-show/app-available-show.module').then(m => m.AppAvailableShowPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId',
|
||||
path: 'installed/:pkgId',
|
||||
loadChildren: () => import('./app-installed-show/app-installed-show.module').then(m => m.AppInstalledShowPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/instructions',
|
||||
path: 'installed/:pkgId/actions',
|
||||
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:pkgId/config',
|
||||
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:pkgId/config/:edit',
|
||||
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:pkgId/instructions',
|
||||
loadChildren: () => import('./app-instructions/app-instructions.module').then(m => m.AppInstructionsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/config',
|
||||
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
|
||||
path: 'installed/:pkgId/interfaces',
|
||||
loadChildren: () => import('./app-interfaces/app-interfaces.module').then(m => m.AppInterfacesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/config/:edit',
|
||||
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/logs',
|
||||
path: 'installed/:pkgId/logs',
|
||||
loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/metrics',
|
||||
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
|
||||
path: 'installed/:pkgId/properties',
|
||||
loadChildren: () => import('./app-properties/app-properties.module').then(m => m.AppPropertiesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'installed/:appId/actions',
|
||||
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
|
||||
path: 'installed/:pkgId/restore',
|
||||
loadChildren: () => import('./app-restore/app-restore.module').then(m => m.AppRestorePageModule),
|
||||
},
|
||||
{
|
||||
path: 'marketplace',
|
||||
loadChildren: () => import('./app-available-list/app-available-list.module').then(m => m.AppAvailableListPageModule),
|
||||
},
|
||||
{
|
||||
path: 'marketplace/:pkgId',
|
||||
loadChildren: () => import('./app-available-show/app-available-show.module').then(m => m.AppAvailableShowPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user