UI/feature/actions (#195)

* ui: actions page

* rework actions page

* add warning to Actions

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Aaron Greenspan
2021-02-15 10:25:01 -07:00
committed by Aiden McClelland
parent 5cf7d1ff88
commit 02ab63da81
17 changed files with 272 additions and 28 deletions

View File

@@ -0,0 +1,32 @@
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 { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: AppActionsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
QRComponentModule,
SharingModule,
],
declarations: [AppActionsPage],
})
export class AppActionsPageModule { }

View File

@@ -0,0 +1,44 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Actions</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
<ng-container *ngIf="!($loading$ | async) && {
title: app.title | async,
versionInstalled: app.versionInstalled | async,
status: app.status | async,
actions: app.actions | async
} as vars">
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<!-- no metrics -->
<ion-item *ngIf="!vars.actions.length">
<ion-label class="ion-text-wrap">
<p>No Actions for {{ vars.title }} {{ vars.versionInstalled }}.</p>
</ion-label>
</ion-item>
<!-- actions -->
<ion-item-group>
<ion-item button *ngFor="let action of vars.actions" (click)="handleAction(action)" >
<ion-label class="ion-text-wrap">
<h2><ion-text color="primary">{{ action.name }}</ion-text><ion-icon *ngIf="!(action.allowedStatuses | includes: vars.status)" color="danger" name="close-outline"></ion-icon></h2>
<p><ion-text color="dark">{{ action.description }}</ion-text></p>
</ion-label>
</ion-item>
</ion-item-group>
</ng-container>
</ion-content>

View File

@@ -0,0 +1,94 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ApiService, isRpcFailure, isRpcSuccess } from 'src/app/services/api/api.service'
import { BehaviorSubject } from 'rxjs'
import { AlertController } from '@ionic/angular'
import { ModelPreload } from 'src/app/models/model-preload'
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { ServiceAction, AppInstalledFull } from 'src/app/models/app-types'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { map } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup'
@Component({
selector: 'app-actions',
templateUrl: './app-actions.page.html',
styleUrls: ['./app-actions.page.scss'],
})
export class AppActionsPage extends Cleanup {
error = ''
$loading$ = new BehaviorSubject(true)
appId: string
app: PropertySubject<AppInstalledFull>
constructor (
private readonly route: ActivatedRoute,
private readonly apiService: ApiService,
private readonly alertCtrl: AlertController,
private readonly preload: ModelPreload,
private readonly loaderService: LoaderService,
) { super() }
ngOnInit () {
this.appId = this.route.snapshot.paramMap.get('appId')
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).pipe(
map(app => this.app = app),
).subscribe( { error: e => this.error = e.message } )
}
async handleAction (action: ServiceAction) {
if (action.allowedStatuses.includes(this.app.status.getValue())) {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: `Are you sure you want to execute action "${action.name}"? ${action.warning}`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Execute',
handler: () => {
this.executeAction(action)
},
},
],
})
await alert.present()
} else {
const alert = await this.alertCtrl.create({
header: 'Forbidden',
message: `Action "${action.name}" can only be executed when service is ${action.allowedStatuses.join(', ')}`,
buttons: ['OK'],
cssClass: 'alert-error-message',
})
await alert.present()
}
}
private async executeAction (action: ServiceAction) {
const res = await this.loaderService.displayDuringP(
this.apiService.serviceAction(this.appId, action),
)
if (isRpcFailure(res)) {
const successAlert = await this.alertCtrl.create({
header: 'Execution Failed',
message: `Error code ${res.error.code}. ${res.error.message}`,
buttons: ['OK'],
cssClass: 'alert-error-message',
})
return await successAlert.present()
}
if (isRpcSuccess(res)) {
const successAlert = await this.alertCtrl.create({
header: 'Execution Complete',
message: res.result,
buttons: ['OK'],
cssClass: 'alert-success-message',
})
return await successAlert.present()
}
}
}

View File

@@ -53,7 +53,7 @@
<p class="ion-text-wrap">Get started by installing your first service.</p>
</div>
<ion-button [routerLink]="['/services','marketplace']" style="width: 50%;" fill="outline">
<ion-icon slot="start" name="cart-outline"></ion-icon>
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>
</div>

View File

@@ -150,6 +150,11 @@
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
</ion-item>
<!-- actions -->
<ion-item [routerLink]="['actions']">
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
</ion-item>
<!-- logs -->
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
@@ -157,7 +162,7 @@
</ion-item>
<!-- marketplace -->
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
<ion-icon slot="start" name="cart-outline" color="primary"></ion-icon>
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
</ion-item>

View File

@@ -13,7 +13,7 @@ import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/a
import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject } from 'rxjs'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { catchError, concatMap, delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators'
import { catchError, concatMap, delay, distinctUntilChanged, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup'
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
import { Emver } from 'src/app/services/emver.service'
@@ -79,6 +79,7 @@ export class AppInstalledShowPage extends Cleanup {
concatMap(app =>
merge(
this.syncWhenDependencyInstalls(),
// new lan info from sync daemon
combineLatest([app.lanEnabled, this.$lanConnected$, app.status, this.$testingLanConnection$]).pipe(
filter(([_, __, s, alreadyConnecting]) => s === AppStatus.RUNNING && !alreadyConnecting),
concatMap(([enabled, connected]) => {
@@ -87,6 +88,22 @@ export class AppInstalledShowPage extends Cleanup {
return of()
}),
),
// toggle lan
combineLatest([this.$lanToggled$, app.lanEnabled, this.$testingLanConnection$]).pipe(
distinctUntilChanged(([toggled1], [toggled2]) => toggled1 !== toggled2),
filter(([_, __, alreadyLoading]) => !alreadyLoading),
map(([e, _]) => [(e as any).detail.checked, _]),
// if the app is already in the desired state, we bail
// this can happen because ionChange triggers when the [checked] value changes
filter(([uiEnabled, appEnabled]) => (uiEnabled && !appEnabled) || (!uiEnabled && appEnabled)),
map(([enabled]) => enabled
? this.enableLan().pipe(concatMap(() => this.testLanConnection()))
: this.disableLan(),
),
concatMap(o => markAsLoadingDuring$(this.$testingLanConnection$, o).pipe(
catchError(e => this.setError(e)),
)),
),
),
), //must be final in stack
catchError(e => this.setError(e)),
@@ -121,22 +138,6 @@ export class AppInstalledShowPage extends Cleanup {
$lanToggled$ = new Subject()
ionViewDidEnter () {
markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())
this.cleanup(
combineLatest([this.$lanToggled$, this.app.lanEnabled, this.$testingLanConnection$]).pipe(
filter(([_, __, alreadyLoading]) => !alreadyLoading),
map(([e, _]) => [(e as any).detail.checked, _]),
// if the app is already in the desired state, we bail
// this can happen because ionChange triggers when the [checked] value changes
filter(([uiEnabled, appEnabled]) => (uiEnabled && !appEnabled) || (!uiEnabled && appEnabled)),
map(([enabled]) => enabled
? this.enableLan().pipe(concatMap(() => this.testLanConnection()))
: this.disableLan(),
),
concatMap(o => markAsLoadingDuring$(this.$testingLanConnection$, o).pipe(
catchError(e => this.setError(e)),
)),
).subscribe({ error: e => console.error(e) }),
)
}
async doRefresh (event: any) {

View File

@@ -43,6 +43,10 @@ const routes: Routes = [
path: 'installed/:appId/metrics',
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
},
{
path: 'installed/:appId/actions',
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
},
]
@NgModule({