* 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:
Matt Hill
2021-08-06 09:25:20 -06:00
committed by Aiden McClelland
parent a43ff976a2
commit 5741cf084f
117 changed files with 1967 additions and 1271 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
.raw {
background-color: var(--ion-color-light);
pre {
margin: 0;
padding: 12px;
white-space: pre-wrap;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
.action-button {
margin: 10px;
min-height: 36px;
min-width: 72px;
min-width: 120px;
}
.icon-spinner {

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ export class LoginPage {
this.loader = await this.loadingCtrl.create({
message: 'Logging in',
spinner: 'lines',
cssClass: 'loader',
})
await this.loader.present()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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