mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
Subnav (#391)
* begin subnav implementation * implement subnav AND angular forms for comparison * unions working-ish, list of enums working * new form approach almost complete * finish new forms approach for action inputs and config * expandable list items and handlebars display * Config animation (#394) * config cammel * config animation Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com> * improve server settings inputs, still needs work * delete all notifications, styling, and bugs * contracted by default Co-authored-by: Drew Ansbacher <drew.ansbacher@gmail.com> Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
This commit is contained in:
committed by
Aiden McClelland
parent
a43ff976a2
commit
5741cf084f
@@ -0,0 +1,7 @@
|
||||
<ion-item button>
|
||||
<ion-icon slot="start" [name]="action.icon"></ion-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h1>{{ action.name }}</h1>
|
||||
<h2>{{ action.description }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
@@ -2,8 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionsPage } from './app-actions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { AppActionInputPageModule } from 'src/app/modals/app-action-input/app-action-input.module'
|
||||
@@ -21,12 +20,14 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
AppActionInputPageModule,
|
||||
AppRestoreComponentModule,
|
||||
],
|
||||
declarations: [AppActionsPage],
|
||||
declarations: [
|
||||
AppActionsPage,
|
||||
AppActionsItemComponent,
|
||||
],
|
||||
})
|
||||
export class AppActionsPageModule { }
|
||||
|
||||
@@ -10,66 +10,37 @@
|
||||
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ng-container *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-item-group *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
|
||||
<ion-grid class="ion-text-center" style="margin: 0 6px;">
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" size="6">
|
||||
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="handleAction(pkg, action)">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>
|
||||
<ion-icon size="large" *ngIf="!(action.value['allowed-statuses'] | includes: pkg.installed.status.main.status); else goodIcon" color="danger" name="close-outline"></ion-icon>
|
||||
<ion-icon size="large" #goAhead name="play-circle-outline"></ion-icon>
|
||||
</ion-card-subtitle>
|
||||
<ion-card-title>{{ action.value.name }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
{{ action.value.description }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
<ion-col size="6">
|
||||
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="restore()">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>
|
||||
<ion-icon size="large" name="color-wand-outline"></ion-icon>
|
||||
</ion-card-subtitle>
|
||||
<ion-card-title>Restore From Backup</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
All changes since backup will be lost.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
<ion-col size="6">
|
||||
<ion-card button style="cursor: pointer !important; height: 88%;" color="light" (click)="uninstall(pkg.manifest)">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>
|
||||
<ion-icon size="large" name="trash-outline"></ion-icon>
|
||||
</ion-card-subtitle>
|
||||
<ion-card-title>Uninstall</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
This will uninstall the service from your Embassy and delete all data permanently.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<!-- ** standard actions ** -->
|
||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||
<app-actions-item
|
||||
[action]="{
|
||||
name: 'Restore From Backup',
|
||||
description: 'All changes since backup will be lost.',
|
||||
icon: 'color-wand-outline'
|
||||
}"
|
||||
(click)="restore()">
|
||||
</app-actions-item>
|
||||
<app-actions-item
|
||||
[action]="{
|
||||
name: 'Uninstall',
|
||||
description: 'This will uninstall the service from your Embassy and delete all data permanently.',
|
||||
icon: 'trash-outline'
|
||||
}"
|
||||
(click)="uninstall(pkg.manifest)">
|
||||
</app-actions-item>
|
||||
|
||||
<!-- <ion-item-group>
|
||||
<ion-item button *ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" (click)="handleAction(pkg, 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: pkg.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 button (click)="uninstall(pkg.manifest)" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="primary">Uninstall</ion-text></h2>
|
||||
<p><ion-text color="dark">This will uninstall the service from your Embassy and delete all data permanently.</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group> -->
|
||||
</ng-container>
|
||||
<!-- ** specific actions ** -->
|
||||
<ion-item-divider>Actions for {{ pkg.manifest.title }}</ion-item-divider>
|
||||
<app-actions-item
|
||||
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder"
|
||||
[action]="{
|
||||
name: action.value.name,
|
||||
description: action.value.description,
|
||||
icon: 'play-circle-outline'
|
||||
}"
|
||||
(click)="handleAction(pkg, action)">
|
||||
</app-actions-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { AlertController, IonContent, LoadingController, ModalController, NavController } from '@ionic/angular'
|
||||
@@ -7,7 +7,6 @@ import { Action, Manifest, PackageDataEntry, PackageMainStatus } from 'src/app/s
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { AppActionInputPage } from 'src/app/modals/app-action-input/app-action-input.page'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AppRestoreComponent } from 'src/app/modals/app-restore/app-restore.component'
|
||||
@@ -49,16 +48,17 @@ export class AppActionsPage {
|
||||
|
||||
async handleAction (pkg: PackageDataEntry, action: { key: string, value: Action }) {
|
||||
if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.installed.status.main.status)) {
|
||||
const inputSpec = action.value['input-spec']
|
||||
if (inputSpec) {
|
||||
if (action.value['input-spec']) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppActionInputPage,
|
||||
componentProps: {
|
||||
action: action.value,
|
||||
cursor: new ConfigCursor(inputSpec, { }),
|
||||
execute: () => this.executeAction(pkg.manifest.id, action.key),
|
||||
},
|
||||
})
|
||||
modal.onWillDismiss().then(({ data }) => {
|
||||
if (!data) return
|
||||
this.executeAction(pkg.manifest.id, action.key, data)
|
||||
})
|
||||
await modal.present()
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
@@ -105,7 +105,7 @@ export class AppActionsPage {
|
||||
}
|
||||
|
||||
async restore (): Promise<void> {
|
||||
const m = await this.modalCtrl.create({
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
pkgId: this.pkgId,
|
||||
},
|
||||
@@ -113,12 +113,12 @@ export class AppActionsPage {
|
||||
backdropDismiss: false,
|
||||
})
|
||||
|
||||
m.onWillDismiss().then(res => {
|
||||
modal.onWillDismiss().then(res => {
|
||||
const data = res.data
|
||||
if (data.error) this.errToast.present(data.error)
|
||||
})
|
||||
|
||||
return await m.present()
|
||||
return await modal.present()
|
||||
}
|
||||
|
||||
async uninstall (manifest: Manifest) {
|
||||
@@ -137,7 +137,7 @@ export class AppActionsPage {
|
||||
return this.navCtrl.navigateRoot('/services')
|
||||
}
|
||||
|
||||
private async executeAction (pkgId: string, actionId: string): Promise<void> {
|
||||
private async executeAction (pkgId: string, actionId: string, input?: object): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Executing action...',
|
||||
@@ -146,7 +146,11 @@ export class AppActionsPage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const res = await this.embassyApi.executePackageAction({ id: pkgId, 'action-id': actionId })
|
||||
const res = await this.embassyApi.executePackageAction({
|
||||
id: pkgId,
|
||||
'action-id': actionId,
|
||||
input,
|
||||
})
|
||||
|
||||
const successAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Complete',
|
||||
@@ -161,3 +165,18 @@ export class AppActionsPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface LocalAction {
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions-item',
|
||||
templateUrl: './app-actions-item.component.html',
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
})
|
||||
export class AppActionsItemComponent {
|
||||
@Input() action: LocalAction
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppConfigPage } from './app-config.page'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
import { AppConfigListPageModule } from 'src/app/modals/app-config-list/app-config-list.module'
|
||||
import { AppConfigObjectPageModule } from 'src/app/modals/app-config-object/app-config-object.module'
|
||||
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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppConfigPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
ObjectConfigComponentModule,
|
||||
AppConfigListPageModule,
|
||||
AppConfigObjectPageModule,
|
||||
AppConfigUnionPageModule,
|
||||
AppConfigValuePageModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
declarations: [AppConfigPage],
|
||||
})
|
||||
export class AppConfigPageModule { }
|
||||
@@ -1,115 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="cancel()">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ pkg.manifest.title }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="loadingText; else loaded" [text]="loadingText"></text-spinner>
|
||||
|
||||
<!-- not loading -->
|
||||
<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><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">
|
||||
<p style="margin-top: 10px; color: var(--ion-color-medium);" [innerHTML]="error.moreInfo.title"></p>
|
||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="error.moreInfo.description"></p>
|
||||
<a style="font-size: x-small; font-style: italic;" (click)="openErrorMoreInfo = false">Hide</a>
|
||||
</ng-container>
|
||||
|
||||
</ion-label>
|
||||
<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="pkg">
|
||||
<!-- @TODO make sure this is how to determine if pkg is in needs_config -->
|
||||
<ng-container *ngIf="pkg.manifest.config && !pkg.installed.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 {{ pkg.manifest.title }}, click "Save" below.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<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-thumbnail style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
||||
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||
</ion-thumbnail>
|
||||
<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)"> {{ pkg.manifest.title }} 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="!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)="dismissRec()">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
</ng-container>
|
||||
|
||||
<ion-item *ngIf="invalid" class="notifier-item">
|
||||
<ion-icon size="small" slot="start" color="danger" name="warning-outline"></ion-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p style="color: var(--ion-color-danger)">{{invalid}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no config -->
|
||||
<ion-item *ngIf="!hasConfig">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No config options for {{ pkg.manifest.title }} {{ pkg.manifest.version }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- save button, always show -->
|
||||
<ion-button
|
||||
[disabled]="invalid || (!edited && !added && !pkg.installed.status.configured )"
|
||||
fill="outline"
|
||||
expand="block"
|
||||
style="margin: 10px"
|
||||
color="primary"
|
||||
(click)="save(pkg)"
|
||||
>
|
||||
<ion-text color="primary" style="font-weight: bold">
|
||||
Save
|
||||
</ion-text>
|
||||
</ion-button>
|
||||
|
||||
<!-- has config -->
|
||||
<ng-container *ngIf="hasConfig">
|
||||
<ion-item-group class="ion-text-wrap ion-padding-bottom">
|
||||
<ion-item-divider>Config Options</ion-item-divider>
|
||||
<object-config [cursor]="rootCursor" (onEdit)="handleObjectEdit()"></object-config>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -1,218 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { NavController, AlertController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { isEmptyObject, Recommendation } from 'src/app/util/misc.util'
|
||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { from, fromEvent, of, Subscription } from 'rxjs'
|
||||
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-config',
|
||||
templateUrl: './app-config.page.html',
|
||||
styleUrls: ['./app-config.page.scss'],
|
||||
})
|
||||
export class AppConfigPage {
|
||||
error: { text: string, moreInfo?:
|
||||
{ title: string, description: string, buttonText: string }
|
||||
}
|
||||
|
||||
loadingText: string | undefined
|
||||
|
||||
pkg: PackageDataEntry
|
||||
hasConfig = false
|
||||
|
||||
mocalShowing = false
|
||||
packageState = PackageState
|
||||
|
||||
rec: Recommendation | null = null
|
||||
showRec = true
|
||||
openRec = false
|
||||
|
||||
invalid: string
|
||||
edited: boolean
|
||||
added: boolean
|
||||
rootCursor: ConfigCursor<'object'>
|
||||
spec: ConfigSpec
|
||||
config: object
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
subs: Subscription[] = []
|
||||
|
||||
constructor (
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly trackingModalCtrl: TrackingModalController,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
const pkgId = this.route.snapshot.paramMap.get('pkgId') as string
|
||||
|
||||
this.subs = [
|
||||
this.route.params.pipe(take(1)).subscribe(params => {
|
||||
if (params.edit) {
|
||||
window.history.back()
|
||||
}
|
||||
}),
|
||||
fromEvent(window, 'popstate').subscribe(() => {
|
||||
this.mocalShowing = false
|
||||
this.trackingModalCtrl.dismissAll()
|
||||
}),
|
||||
this.trackingModalCtrl.onCreateAny$().subscribe(() => {
|
||||
if (!this.mocalShowing) {
|
||||
window.history.pushState(null, null, window.location.href + '/edit')
|
||||
this.mocalShowing = true
|
||||
}
|
||||
}),
|
||||
this.trackingModalCtrl.onDismissAny$().subscribe(() => {
|
||||
if (!this.trackingModalCtrl.anyModals && this.mocalShowing === true) {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
}),
|
||||
this.patch.watch$('package-data', pkgId)
|
||||
.pipe(
|
||||
tap(pkg => this.pkg = pkg),
|
||||
tap(() => this.loadingText = 'Loading config...'),
|
||||
concatMap(() => this.embassyApi.getPackageConfig({ id: pkgId })),
|
||||
concatMap(({ spec, config }) => {
|
||||
const rec = history.state && history.state.configRecommendation as Recommendation
|
||||
if (rec) {
|
||||
this.loadingText = `Setting properties to accommodate ${rec.dependentTitle}...`
|
||||
return from(this.embassyApi.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 })
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
return of({ spec, config, dependencyConfig: null })
|
||||
}
|
||||
}),
|
||||
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
|
||||
tap(() => this.loadingText = undefined),
|
||||
take(1),
|
||||
).subscribe({
|
||||
error: e => {
|
||||
console.error(e.message)
|
||||
this.error = { text: e.message }
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
setConfig (spec: ConfigSpec, config: object, dependencyConfig?: object) {
|
||||
this.rootCursor = dependencyConfig ? new ConfigCursor(spec, config, null, dependencyConfig) : new ConfigCursor(spec, config)
|
||||
this.spec = this.rootCursor.spec().spec
|
||||
this.config = this.rootCursor.config()
|
||||
this.handleObjectEdit()
|
||||
this.hasConfig = !isEmptyObject(this.spec)
|
||||
}
|
||||
|
||||
dismissRec () {
|
||||
this.showRec = false
|
||||
}
|
||||
|
||||
dismissError () {
|
||||
this.error = undefined
|
||||
}
|
||||
|
||||
async cancel () {
|
||||
if (this.edited) {
|
||||
await this.presentAlertUnsaved()
|
||||
} else {
|
||||
this.navCtrl.back()
|
||||
}
|
||||
}
|
||||
|
||||
async save (pkg: PackageDataEntry) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: `Saving config...`,
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const breakages = await this.embassyApi.drySetPackageConfig({ id: pkg.manifest.id, config: this.config })
|
||||
|
||||
if (!isEmptyObject(breakages.length)) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalController,
|
||||
this.wizardBaker.configure({
|
||||
pkg,
|
||||
breakages,
|
||||
}),
|
||||
)
|
||||
if (cancelled) return
|
||||
}
|
||||
|
||||
await this.embassyApi.setPackageConfig({ id: pkg.manifest.id, config: this.config })
|
||||
this.navCtrl.back()
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
handleObjectEdit () {
|
||||
this.edited = this.rootCursor.isEdited()
|
||||
this.added = this.rootCursor.isNew()
|
||||
this.invalid = this.rootCursor.checkInvalid()
|
||||
}
|
||||
|
||||
private async presentAlertUnsaved () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Unsaved Changes',
|
||||
message: 'You have unsaved changes. Are you sure you want to leave?',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: `Leave`,
|
||||
handler: () => {
|
||||
this.navCtrl.back()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppInstructionsPage } from './app-instructions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [
|
||||
AppInstructionsPage,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<ion-item>
|
||||
<ion-icon slot="start" [name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'"></ion-icon>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h1>{{ interface.def.name }}</h1>
|
||||
<h2>{{ interface.def.description }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div style="padding-left: 54px;">
|
||||
<!-- has tor -->
|
||||
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>{{ tor }}</p>
|
||||
</ion-label>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(tor)">
|
||||
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copy(tor)">
|
||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-item>
|
||||
<!-- no tor -->
|
||||
<ion-item *ngIf="!interface.addresses['tor-address']">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>Service does not use a Tor Address</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- lan -->
|
||||
<ion-item *ngIf="interface.addresses['lan-address'] as lan">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p>{{ lan }}</p>
|
||||
</ion-label>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(lan)">
|
||||
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copy(lan)">
|
||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-item>
|
||||
<!-- no lan -->
|
||||
<ion-item *ngIf="!interface.addresses['lan-address']">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p>Service does not use a LAN Address</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
@@ -2,8 +2,7 @@ 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 { AppInterfacesItemComponent, AppInterfacesPage } from './app-interfaces.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -18,9 +17,11 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppInterfacesPage],
|
||||
declarations: [
|
||||
AppInterfacesPage,
|
||||
AppInterfacesItemComponent,
|
||||
],
|
||||
})
|
||||
export class AppInterfacesPageModule { }
|
||||
|
||||
@@ -7,37 +7,20 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="patch.data['package-data'][pkgId] as pkg">
|
||||
<ion-card *ngFor="let interface of pkg.manifest.interfaces | keyvalue: asIsOrder">
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{ interface.value.name }}</ion-card-title>
|
||||
<ion-card-subtitle>{{ interface.value.description }}</ion-card-subtitle>
|
||||
<ion-button style="margin-top: 12px;" *ngIf="interface.value.ui" [disabled]="!(pkg | isLaunchable)" fill="outline" color="dark" expand="block" (click)="launch(pkg)">
|
||||
Launch
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</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>
|
||||
</ng-container>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group>
|
||||
<!-- iff ui -->
|
||||
<ng-container *ngIf="ui">
|
||||
<ion-item-divider>Web User Interface</ion-item-divider>
|
||||
<app-interfaces-item [interface]="ui"></app-interfaces-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- other interface -->
|
||||
<ng-container *ngIf="other.length">
|
||||
<ion-item-divider>Other Interfaces</ion-item-divider>
|
||||
<div *ngFor="let interface of other" style="margin-bottom: 30px;">
|
||||
<app-interfaces-item [interface]="interface"></app-interfaces-item>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,38 +1,87 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { IonContent, ToastController } from '@ionic/angular'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { InstalledPackageDataEntry, PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { InterfaceDef, InterfaceInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
|
||||
interface LocalInterface {
|
||||
def: InterfaceDef
|
||||
addresses: InterfaceInfo['addresses'][string]
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-Interfaces',
|
||||
templateUrl: './app-Interfaces.page.html',
|
||||
styleUrls: ['./app-Interfaces.page.scss'],
|
||||
selector: 'app-interfaces',
|
||||
templateUrl: './app-interfaces.page.html',
|
||||
styleUrls: ['./app-interfaces.page.scss'],
|
||||
})
|
||||
export class AppInterfacesPage {
|
||||
pkg: PackageDataEntry
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
pkgId: string
|
||||
ui: LocalInterface | null
|
||||
other: LocalInterface[]
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly config: ConfigService,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
const pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
const pkg = this.patch.data['package-data'][pkgId]
|
||||
const interfaces = pkg.manifest.interfaces
|
||||
const addressesMap = pkg.installed['interface-info'].addresses
|
||||
const ui = interfaces['ui']
|
||||
|
||||
if (ui) {
|
||||
const uiAddresses = addressesMap['ui']
|
||||
this.ui = {
|
||||
def: ui,
|
||||
addresses: {
|
||||
'lan-address': uiAddresses['lan-address'] ? 'https://' + uiAddresses['lan-address'] : null,
|
||||
'tor-address': uiAddresses['tor-address'] ? 'http://' + uiAddresses['tor-address'] : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
this.other = Object.keys(interfaces)
|
||||
.filter(key => key !== 'ui')
|
||||
.map(key => {
|
||||
const addresses = addressesMap[key]
|
||||
return {
|
||||
def: interfaces[key],
|
||||
addresses: {
|
||||
'lan-address': addresses['lan-address'] ? 'https://' + addresses['lan-address'] : null,
|
||||
'tor-address': addresses['tor-address'] ? 'http://' + addresses['tor-address'] : null,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-interfaces-item',
|
||||
templateUrl: './app-interfaces-item.component.html',
|
||||
styleUrls: ['./app-interfaces.page.scss'],
|
||||
})
|
||||
export class AppInterfacesItemComponent {
|
||||
@Input() interface: LocalInterface
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
) { }
|
||||
|
||||
launch (url: string): void {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
async copy (address: string): Promise<void> {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
@@ -45,12 +94,4 @@ export class AppInterfacesPage {
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
launch (pkg: PackageDataEntry): void {
|
||||
window.open(this.config.launchableURL(pkg), '_blank')
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<ion-card class="installed-card" [routerLink]="['/services', pkg.value.entry.manifest.id]">
|
||||
<div class="launch-container" *ngIf="pkg.value.entry | hasUi">
|
||||
<div class="launch-button-triangle" (click)="launchUi(pkg.value.entry, $event)" [class.launch-disabled]="!(pkg.value.entry | isLaunchable)">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppLogsPage } from './app-logs.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,8 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppLogsPage],
|
||||
})
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppManifestPage } from './app-manifest.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppManifestPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppManifestPage],
|
||||
})
|
||||
export class AppManifestPageModule { }
|
||||
@@ -1,67 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Package Manifest</ion-title>
|
||||
</ion-toolbar>
|
||||
<ion-toolbar>
|
||||
<ion-segment [(ngModel)]="segmentValue">
|
||||
<ion-segment-button value="formatted">
|
||||
<ion-label>Formatted</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="raw">
|
||||
<ion-label>Raw</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content *ngIf="pkg" class="ion-padding">
|
||||
|
||||
<div *ngIf="segmentValue === 'formatted'" style="background-color: var(--ion-color-light);">
|
||||
<ion-toolbar>
|
||||
<ion-title>Formatted Manifest</ion-title>
|
||||
<ion-buttons slot="start" *ngIf="!!pointer">
|
||||
<ion-button (click)="handleFormattedBack()">
|
||||
<ion-icon slot="icon-only" name="arrow-back-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
<!-- node is object -->
|
||||
<ion-item-group *ngIf="(node | typeof) === 'object'">
|
||||
<div *ngFor="let prop of node | keyvalue : asIsOrder">
|
||||
<!-- object/array -->
|
||||
<ng-container *ngIf="['object', 'array'] | includes : (prop.value | typeof); else notObj">
|
||||
<ion-item button detail="true" *ngIf="!(prop.value | empty)" (click)="goToNested(prop.key)">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ prop.key }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<!-- not object/array -->
|
||||
<ng-template #notObj>
|
||||
<ion-item *ngIf="prop.value">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>{{ prop.key }}</h2>
|
||||
<p>{{ prop.value }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
<!-- node is array -->
|
||||
<ion-item-group *ngIf="(node | typeof) === 'array'">
|
||||
<ion-item *ngFor="let prop of node">
|
||||
<ion-label class="ion-text-wrap">
|
||||
{{ prop }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="segmentValue === 'raw'" class="raw">
|
||||
<pre [innerHTML]="pkg.manifest | json"></pre>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.raw {
|
||||
background-color: var(--ion-color-light);
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import * as JsonPointer from 'json-pointer'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'app-manifest',
|
||||
templateUrl: './app-manifest.page.html',
|
||||
styleUrls: ['./app-manifest.page.scss'],
|
||||
})
|
||||
export class AppManifestPage {
|
||||
pkg: PackageDataEntry
|
||||
pointer: string
|
||||
node: object
|
||||
segmentValue: 'formatted' | 'raw' = 'formatted'
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
subs: Subscription[] = []
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
const pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||
|
||||
this.subs = [
|
||||
this.patch.watch$('package-data', pkgId)
|
||||
.subscribe(pkg => {
|
||||
this.pkg = pkg
|
||||
this.setNode()
|
||||
}),
|
||||
]
|
||||
|
||||
this.setNode()
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
handleFormattedBack () {
|
||||
const arr = this.pointer.split('/')
|
||||
arr.pop()
|
||||
this.pointer = arr.join('/')
|
||||
this.setNode()
|
||||
}
|
||||
|
||||
private setNode () {
|
||||
this.node = JsonPointer.get(this.pkg.manifest, this.pointer || '')
|
||||
}
|
||||
|
||||
async goToNested (key: string): Promise<any> {
|
||||
this.pointer = `${this.pointer || ''}/${key}`
|
||||
this.setNode()
|
||||
}
|
||||
|
||||
asIsOrder (a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppMetricsPage } from './app-metrics.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
|
||||
@@ -19,7 +18,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
SkeletonListComponentModule,
|
||||
],
|
||||
|
||||
@@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
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'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,10 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [AppPropertiesPage],
|
||||
})
|
||||
|
||||
@@ -5,8 +5,8 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { AppShowPage } from './app-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,11 +19,11 @@ const routes: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
StatusComponentModule,
|
||||
SharingModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
InstallWizardComponentModule,
|
||||
AppConfigPageModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppShowPage],
|
||||
})
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<status size="x-large" weight="500" [rendering]="rendering"></status>
|
||||
</ion-label>
|
||||
<ion-button slot="end" class="action-button" *ngIf="pkg.state === PackageState.Installed && (pkg | hasUi)" [disabled]="!(pkg | isLaunchable)" (click)="launchUiTab()">
|
||||
Web
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
<ion-icon slot="start" name="open-outline"></ion-icon>
|
||||
Open UI
|
||||
</ion-button>
|
||||
<ion-button slot="end" class="action-button" *ngIf="rendering.feStatus === FeStatus.NeedsConfig" [routerLink]="['config']">
|
||||
Configure
|
||||
@@ -53,7 +53,7 @@
|
||||
<ion-item *ngFor="let health of mainStatus.health | keyvalue : asIsOrder">
|
||||
<ion-spinner class="icon-spinner" color="warning" slot="start" *ngIf="['starting', 'loading'] | includes : health.value.result"></ion-spinner>
|
||||
<ion-icon slot="start" *ngIf="health.value.result === 'success'" name="checkmark-outline" color="success"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="health.value.result === 'failure'" name="close-outline" color="danger"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="health.value.result === 'failure'" name="close" color="danger"></ion-icon>
|
||||
<ion-icon slot="start" *ngIf="health.value.result === 'disabled'" name="remove-outline" color="dark"></ion-icon>
|
||||
<ion-label>
|
||||
<p>{{ health.key }}</p>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.action-button {
|
||||
margin: 10px;
|
||||
min-height: 36px;
|
||||
min-width: 72px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, ViewChild } from '@angular/core'
|
||||
import { AlertController, NavController, ModalController, IonContent, LoadingController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { chill, isEmptyObject, Recommendation } from 'src/app/util/misc.util'
|
||||
import { isEmptyObject, Recommendation } from 'src/app/util/misc.util'
|
||||
import { combineLatest, Subscription } from 'rxjs'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
@@ -12,6 +12,7 @@ import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, Dependen
|
||||
import { FEStatus, PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { AppConfigPage } from 'src/app/modals/app-config/app-config.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show',
|
||||
@@ -31,7 +32,6 @@ export class AppShowPage {
|
||||
Math = Math
|
||||
mainStatus: MainStatus
|
||||
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
subs: Subscription[] = []
|
||||
|
||||
@@ -64,21 +64,20 @@ export class AppShowPage {
|
||||
this.patch.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.subscribe(main => {
|
||||
this.mainStatus = main
|
||||
console.log(this.mainStatus)
|
||||
}),
|
||||
]
|
||||
this.setButtons()
|
||||
}
|
||||
|
||||
// ngAfterViewInit () {
|
||||
// this.content.scrollToPoint(undefined, 1)
|
||||
// }
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
launchUiTab (): void {
|
||||
launchUi (): void {
|
||||
window.open(this.config.launchableURL(this.pkg), '_blank')
|
||||
}
|
||||
|
||||
@@ -94,8 +93,6 @@ export class AppShowPage {
|
||||
try {
|
||||
const breakages = await this.embassyApi.dryStopPackage({ id })
|
||||
|
||||
console.log('BREAKAGES', breakages)
|
||||
|
||||
if (!isEmptyObject(breakages)) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
@@ -108,7 +105,7 @@ export class AppShowPage {
|
||||
)
|
||||
if (cancelled) return
|
||||
}
|
||||
return this.embassyApi.stopPackage({ id }).then(chill)
|
||||
await this.embassyApi.stopPackage({ id })
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -156,8 +153,14 @@ export class AppShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
async presentModalConfig (): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppConfigPage,
|
||||
componentProps: {
|
||||
pkgId: this.pkgId,
|
||||
},
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async installDep (depId: string): Promise<void> {
|
||||
@@ -234,8 +237,9 @@ export class AppShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
setButtons (): void {
|
||||
private setButtons (): void {
|
||||
this.buttons = [
|
||||
// instructions
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route }),
|
||||
title: 'Instructions',
|
||||
@@ -243,13 +247,15 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// config
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['config'], { relativeTo: this.route }),
|
||||
title: 'Settings',
|
||||
action: async () => this.presentModalConfig(),
|
||||
title: 'Config',
|
||||
icon: 'construct-outline',
|
||||
color: 'danger',
|
||||
disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring],
|
||||
},
|
||||
// properties
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['properties'], { relativeTo: this.route }),
|
||||
title: 'Properties',
|
||||
@@ -257,6 +263,7 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// interfaces
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['interfaces'], { relativeTo: this.route }),
|
||||
title: 'Interfaces',
|
||||
@@ -264,6 +271,7 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// actions
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||
title: 'Actions',
|
||||
@@ -271,6 +279,7 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
// metrics
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
||||
title: 'Monitor',
|
||||
@@ -279,6 +288,7 @@ export class AppShowPage {
|
||||
// @TODO make the disabled check better. Don't want to list every status here. Monitor should be disabled except is pkg is running.
|
||||
disabled: [FEStatus.Installing, FEStatus.Updating, FEStatus.Removing, FEStatus.BackingUp, FEStatus.Restoring],
|
||||
},
|
||||
// logs
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
title: 'Logs',
|
||||
@@ -286,22 +296,19 @@ export class AppShowPage {
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
{
|
||||
action: () => this.navCtrl.navigateForward(['manifest'], { relativeTo: this.route }),
|
||||
title: 'Package Details',
|
||||
icon: 'finger-print-outline',
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
{
|
||||
action: () => this.donate(),
|
||||
title: 'Donate',
|
||||
title: `Donate to ${this.pkg.manifest.title}`,
|
||||
icon: 'logo-bitcoin',
|
||||
color: 'danger',
|
||||
disabled: [],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
interface Button {
|
||||
|
||||
@@ -19,14 +19,6 @@ const routes: Routes = [
|
||||
path: ':pkgId/actions',
|
||||
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/config',
|
||||
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/config/:edit',
|
||||
loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/instructions',
|
||||
loadChildren: () => import('./app-instructions/app-instructions.module').then(m => m.AppInstructionsPageModule),
|
||||
@@ -39,10 +31,6 @@ const routes: Routes = [
|
||||
path: ':pkgId/logs',
|
||||
loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/manifest',
|
||||
loadChildren: () => import('./app-manifest/app-manifest.module').then(m => m.AppManifestPageModule),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/metrics',
|
||||
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ion-row class="ion-align-items-center" style="height: 100%;">
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
<div style="padding-bottom: 16px;">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px;" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class LoginPage {
|
||||
this.loader = await this.loadingCtrl.create({
|
||||
message: 'Logging in',
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await this.loader.present()
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppReleaseNotes } from './app-release-notes.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [AppReleaseNotes],
|
||||
})
|
||||
|
||||
@@ -6,8 +6,6 @@ import { MarketplaceListPage } from './marketplace-list.page'
|
||||
import { SharingModule } from '../../../modules/sharing.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 { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -24,7 +22,6 @@ const routes: Routes = [
|
||||
StatusComponentModule,
|
||||
SharingModule,
|
||||
BadgeMenuComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [MarketplaceListPage],
|
||||
})
|
||||
|
||||
@@ -4,10 +4,8 @@ import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -21,10 +19,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
StatusComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
InstallWizardComponentModule,
|
||||
],
|
||||
declarations: [MarketplaceShowPage],
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
<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 style="position: absolute; right: 0; top: 0" fill="clear" (click)="dismissRec()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
|
||||
@@ -3,10 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { NotificationsPage } from './notifications.page'
|
||||
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 { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,10 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [NotificationsPage],
|
||||
})
|
||||
|
||||
@@ -17,41 +17,52 @@
|
||||
|
||||
<text-spinner *ngIf="loading" text="Loading Notifications"></text-spinner>
|
||||
|
||||
<ion-item-group *ngIf="!notifications.length && !loading">
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
Notifications about Embassy and services will appear here.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-group style="margin-bottom: 16px;">
|
||||
<ion-item *ngFor="let not of notifications; let i = index">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>
|
||||
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
|
||||
</h2>
|
||||
<h2 class="notification-message">
|
||||
{{ not.message }}
|
||||
<a *ngIf="not.code === 1" style="text-decoration: none;" (click)="viewBackupReport(not)">
|
||||
View Report
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
{{ not['created-at'] | date: 'short' }}
|
||||
<a *ngIf="not['package-id'] as pkgId" style="text-decoration: none;" [routerLink]="['/services', not['package-id']]">
|
||||
- {{ not['package-id'] }}
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="remove(not.id, i)">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-infinite-scroll [disabled]="!needInfinite" (ionInfinite)="doInfinite($event)">
|
||||
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
<!-- no notifications -->
|
||||
<ng-container *ngIf="!loading">
|
||||
<ion-item-group *ngIf="!notifications.length">
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
Notifications about Embassy and services will appear here.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- has notifications -->
|
||||
<ng-container *ngIf="notifications.length">
|
||||
<ion-item-group style="margin-bottom: 16px;">
|
||||
<ion-item-divider>
|
||||
<ion-button slot="end" fill="clear" (click)="deleteAll()">
|
||||
Delete All
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let not of notifications; let i = index">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>
|
||||
<ion-text [color]="not | notificationColor"><b>{{ not.title }}</b></ion-text>
|
||||
</h2>
|
||||
<h2 class="notification-message">
|
||||
{{ not.message }}
|
||||
<a *ngIf="not.code === 1" style="text-decoration: none;" (click)="viewBackupReport(not)">
|
||||
View Report
|
||||
</a>
|
||||
</h2>
|
||||
<p>
|
||||
{{ not['created-at'] | date: 'short' }}
|
||||
<a *ngIf="not['package-id'] as pkgId" style="text-decoration: none;" [routerLink]="['/services', not['package-id']]">
|
||||
- {{ not['package-id'] }}
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="delete(not.id, i)">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-infinite-scroll [disabled]="!needInfinite" (ionInfinite)="doInfinite($event)">
|
||||
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</ion-content>
|
||||
@@ -57,7 +57,7 @@ export class NotificationsPage {
|
||||
}
|
||||
}
|
||||
|
||||
async remove (id: string, index: number): Promise<void> {
|
||||
async delete (id: string, index: number): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Deleting...',
|
||||
@@ -75,6 +75,24 @@ export class NotificationsPage {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAll (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Deleting...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.deleteAllNotifications({ })
|
||||
this.notifications = []
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async viewBackupReport (notification: ServerNotification<1>) {
|
||||
const data = notification.data
|
||||
|
||||
@@ -99,9 +117,6 @@ export class NotificationsPage {
|
||||
if (embassyFailed || packagesFailed) {
|
||||
buttons.push({
|
||||
text: 'Retry',
|
||||
handler: () => {
|
||||
console.log('retry backup')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { LANPage } from './lan.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 = [
|
||||
@@ -18,7 +17,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [LANPage],
|
||||
|
||||
@@ -3,8 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SecurityOptionsPage } from './security-options.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -18,9 +16,7 @@ const routes: Routes = [
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
ObjectConfigComponentModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
@@ -11,23 +11,23 @@
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item button (click)="presentModalValueEdit('shareStats', patch.data['server-info']['share-stats'])">
|
||||
<ion-item button (click)="serverConfig.presentAlert('share-stats', server['share-stats'])">
|
||||
<ion-label>Share Anonymous Statistics</ion-label>
|
||||
<ion-note slot="end">{{ patch.data['server-info']['share-stats'] }}</ion-note>
|
||||
<ion-note slot="end">{{ server['share-stats'] ? 'Enabled' : 'Disabled' }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
<ion-item button (click)="presentModalValueEdit('autoCheckUpdates', patch.data.ui['auto-check-updates'])">
|
||||
<ion-item button (click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'])">
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] }}</ion-note>
|
||||
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item button (click)="presentModalValueEdit('eosMarketplace', patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor)">
|
||||
<ion-item button (click)="serverConfig.presentAlert('eos-marketplace', server['eos-marketplace'] === config.start9Marketplace.tor)">
|
||||
<ion-label>Tor Only Marketplace</ion-label>
|
||||
<ion-note slot="end">{{ patch.data['server-info']['eos-marketplace'] === config.start9Marketplace.tor }}</ion-note>
|
||||
<ion-note slot="end">{{ server['eos-marketplace'] === config.start9Marketplace.tor ? 'Enabled' : 'Disabled' }}</ion-note>
|
||||
</ion-item>
|
||||
<!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', patch.data['server-info']['package-marketplace'])">
|
||||
<!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', server['package-marketplace'])">
|
||||
<ion-label>Package Marketplace</ion-label>
|
||||
<ion-note slot="end">{{ patch.data['server-info']['package-marketplace'] }}</ion-note>
|
||||
<ion-note slot="end">{{ server['package-marketplace'] }}</ion-note>
|
||||
</ion-item> -->
|
||||
|
||||
<ion-item-divider>Security</ion-item-divider>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'security-options',
|
||||
@@ -9,14 +10,15 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
styleUrls: ['./security-options.page.scss'],
|
||||
})
|
||||
export class SecurityOptionsPage {
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
public readonly serverConfig: ServerConfigService,
|
||||
public readonly config: ConfigService,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
async presentModalValueEdit (key: string, current?: any): Promise<void> {
|
||||
await this.serverConfigService.presentModalValueEdit(key, current)
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { SessionsPage } from './sessions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [SessionsPage],
|
||||
})
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
<h2>Last Active: {{ session.value['last-active'] | date : 'medium' }}</h2>
|
||||
<p>{{ session.value['user-agent'] }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="presentAlertKill(session.key)">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
<ion-button slot="end" fill="clear" color="danger" (click)="presentAlertKill(session.key)">
|
||||
<ion-icon slot="start" name="close"></ion-icon>
|
||||
Kill
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { AlertController, getPlatforms, LoadingController } from '@ionic/angular'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { PlatformType, RR, SessionMetadata } from 'src/app/services/api/api.types'
|
||||
import { PlatformType, RR } from 'src/app/services/api/api.types'
|
||||
|
||||
@Component({
|
||||
selector: 'sessions',
|
||||
|
||||
@@ -3,9 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { SSHKeysPage } from './ssh-keys.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -19,9 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
TextSpinnerComponentModule,
|
||||
],
|
||||
declarations: [SSHKeysPage],
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</ion-buttons>
|
||||
<ion-title>SSH Keys</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="presentModalAdd()">
|
||||
<ion-button (click)="serverConfig.presentAlert('ssh')">
|
||||
<ion-icon slot="icon-only" name="add-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
@@ -15,14 +15,25 @@
|
||||
<ion-content class="ion-padding-top">
|
||||
<text-spinner *ngIf="loading" text="Loading Keys"></text-spinner>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<!-- about -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p class="ion-padding-bottom">About</p>
|
||||
<h2>Adding an SSH key to your Embassy can be useful for advanced usage from the command line, as well as for debugging purposes.</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item [href]="docsUrl" target="_blank" detail="false">
|
||||
<ion-icon slot="start" name="list-outline"></ion-icon>
|
||||
<ion-label>View Instructions</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider>Saved Keys</ion-item-divider>
|
||||
<ion-item *ngFor="let ssh of sshKeys | keyvalue : asIsOrder">
|
||||
<ion-label class="ion-text-wrap">
|
||||
{{ ssh.value.alg }} {{ ssh.key }} {{ ssh.value.hostname }}
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="presentAlertDelete(ssh.key)">
|
||||
<ion-icon slot="icon-only" name="close-outline"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
@@ -15,13 +15,14 @@ export class SSHKeysPage {
|
||||
loading = true
|
||||
sshKeys: SSHKeys
|
||||
subs: Subscription[] = []
|
||||
readonly docsUrl = 'https://docs.start9.com/user-manual/general/developer-options/ssh-setup.html'
|
||||
|
||||
constructor (
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly serverConfigService: ServerConfigService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly sshService: SSHService,
|
||||
public readonly serverConfig: ServerConfigService,
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -41,10 +42,6 @@ export class SSHKeysPage {
|
||||
this.subs.forEach(sub => sub.unsubscribe())
|
||||
}
|
||||
|
||||
async presentModalAdd () {
|
||||
await this.serverConfigService.presentModalValueEdit('ssh')
|
||||
}
|
||||
|
||||
async presentAlertDelete (hash: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
|
||||
@@ -4,8 +4,7 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { ServerBackupPage } from './server-backup.page'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { BackupConfirmationComponentModule } from 'src/app/modals/backup-confirmation/backup-confirmation.component.module'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,8 +19,7 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
BackupConfirmationComponentModule,
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [
|
||||
ServerBackupPage,
|
||||
|
||||
@@ -3,8 +3,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerLogsPage } from './server-logs.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { TextSpinnerComponentModule } from 'src/app/components/text-spinner/text-spinner.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,8 +17,7 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
TextSpinnerComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerLogsPage],
|
||||
})
|
||||
|
||||
@@ -3,8 +3,8 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerMetricsPage } from './server-metrics.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -18,8 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SkeletonListComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerMetricsPage],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonContent } from '@ionic/angular'
|
||||
import { Metrics } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
@@ -13,6 +14,7 @@ export class ServerMetricsPage {
|
||||
loading = true
|
||||
going = false
|
||||
metrics: Metrics = { }
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly errToast: ErrorToastService,
|
||||
@@ -23,6 +25,10 @@ export class ServerMetricsPage {
|
||||
this.startDaemon()
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.stopDaemon()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ServerShowPage } from './server-show.page'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
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'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -24,7 +23,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharingModule,
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
],
|
||||
declarations: [ServerShowPage],
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ServerSpecsPage } from './server-specs.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 = [
|
||||
@@ -18,7 +17,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [ServerSpecsPage],
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonContent, ToastController } from '@ionic/angular'
|
||||
import { copyToClipboard } from 'src/app/util/web.util'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'server-specs',
|
||||
@@ -10,13 +9,17 @@ import { Subscription } from 'rxjs'
|
||||
styleUrls: ['./server-specs.page.scss'],
|
||||
})
|
||||
export class ServerSpecsPage {
|
||||
subs: Subscription[] = []
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.content.scrollToPoint(undefined, 1)
|
||||
}
|
||||
|
||||
async copy (address: string) {
|
||||
let message = ''
|
||||
await copyToClipboard(address || '')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { WifiAddPage } from './wifi-add.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 = [
|
||||
{
|
||||
@@ -19,7 +19,7 @@ const routes: Routes = [
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [WifiAddPage],
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { WifiListPage } from './wifi.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 = [
|
||||
@@ -22,7 +21,6 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [WifiListPage],
|
||||
|
||||
Reference in New Issue
Block a user