mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
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:
committed by
Aiden McClelland
parent
5cf7d1ff88
commit
02ab63da81
@@ -70,7 +70,6 @@
|
|||||||
<ion-icon name="arrow-forward"></ion-icon>
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
<ion-icon name="arrow-up"></ion-icon>
|
<ion-icon name="arrow-up"></ion-icon>
|
||||||
<ion-icon name="bookmark-outline"></ion-icon>
|
<ion-icon name="bookmark-outline"></ion-icon>
|
||||||
<ion-icon name="cart-outline"></ion-icon>
|
|
||||||
<ion-icon name="chevron-down"></ion-icon>
|
<ion-icon name="chevron-down"></ion-icon>
|
||||||
<ion-icon name="chevron-up"></ion-icon>
|
<ion-icon name="chevron-up"></ion-icon>
|
||||||
<ion-icon name="close"></ion-icon>
|
<ion-icon name="close"></ion-icon>
|
||||||
@@ -86,6 +85,7 @@
|
|||||||
<ion-icon name="eye-off-outline"></ion-icon>
|
<ion-icon name="eye-off-outline"></ion-icon>
|
||||||
<ion-icon name="eye-outline"></ion-icon>
|
<ion-icon name="eye-outline"></ion-icon>
|
||||||
<ion-icon name="file-tray-stacked-outline"></ion-icon>
|
<ion-icon name="file-tray-stacked-outline"></ion-icon>
|
||||||
|
<ion-icon name="flash-outline"></ion-icon>
|
||||||
<ion-icon name="grid-outline"></ion-icon>
|
<ion-icon name="grid-outline"></ion-icon>
|
||||||
<ion-icon name="help-circle-outline"></ion-icon>
|
<ion-icon name="help-circle-outline"></ion-icon>
|
||||||
<ion-icon name="home-outline"></ion-icon>
|
<ion-icon name="home-outline"></ion-icon>
|
||||||
@@ -100,6 +100,7 @@
|
|||||||
<ion-icon name="reload-outline"></ion-icon>
|
<ion-icon name="reload-outline"></ion-icon>
|
||||||
<ion-icon name="refresh-outline"></ion-icon>
|
<ion-icon name="refresh-outline"></ion-icon>
|
||||||
<ion-icon name="save-outline"></ion-icon>
|
<ion-icon name="save-outline"></ion-icon>
|
||||||
|
<ion-icon name="storefront-outline"></ion-icon>
|
||||||
<ion-icon name="terminal-outline"></ion-icon>
|
<ion-icon name="terminal-outline"></ion-icon>
|
||||||
<ion-icon name="trash-outline"></ion-icon>
|
<ion-icon name="trash-outline"></ion-icon>
|
||||||
<ion-icon name="warning-outline"></ion-icon>
|
<ion-icon name="warning-outline"></ion-icon>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class AppComponent {
|
|||||||
{
|
{
|
||||||
title: 'Marketplace',
|
title: 'Marketplace',
|
||||||
url: '/services/marketplace',
|
url: '/services/marketplace',
|
||||||
icon: 'cart-outline',
|
icon: 'storefront-outline',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Notifications',
|
title: 'Notifications',
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPrev
|
|||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
configuredRequirements: null,
|
configuredRequirements: null,
|
||||||
hasFetchedFull: false,
|
hasFetchedFull: false,
|
||||||
|
actions: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,17 @@ export interface AppInstalledFull extends AppInstalledPreview {
|
|||||||
hasFetchedFull: boolean
|
hasFetchedFull: boolean
|
||||||
uninstallAlert?: string
|
uninstallAlert?: string
|
||||||
restoreAlert?: string
|
restoreAlert?: string
|
||||||
|
actions: Actions
|
||||||
}
|
}
|
||||||
// dependencies
|
|
||||||
|
|
||||||
|
export type Actions = ServiceAction[]
|
||||||
|
export interface ServiceAction {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
warning?: string
|
||||||
|
allowedStatuses: AppStatus[]
|
||||||
|
}
|
||||||
export interface AppDependency extends InstalledAppDependency {
|
export interface AppDependency extends InstalledAppDependency {
|
||||||
// explanation of why it *is* optional. null represents it is required.
|
// explanation of why it *is* optional. null represents it is required.
|
||||||
optional: string | null
|
optional: string | null
|
||||||
|
|||||||
@@ -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 { }
|
||||||
@@ -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>
|
||||||
94
ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts
Normal file
94
ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||||
</div>
|
</div>
|
||||||
<ion-button [routerLink]="['/services','marketplace']" style="width: 50%;" fill="outline">
|
<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
|
Marketplace
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -150,6 +150,11 @@
|
|||||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||||
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
|
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
|
||||||
</ion-item>
|
</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 -->
|
<!-- logs -->
|
||||||
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
|
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
|
||||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||||
@@ -157,7 +162,7 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- marketplace -->
|
<!-- marketplace -->
|
||||||
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
|
<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-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/a
|
|||||||
import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject } from 'rxjs'
|
import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject } from 'rxjs'
|
||||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
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 { Cleanup } from 'src/app/util/cleanup'
|
||||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||||
import { Emver } from 'src/app/services/emver.service'
|
import { Emver } from 'src/app/services/emver.service'
|
||||||
@@ -79,6 +79,7 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
concatMap(app =>
|
concatMap(app =>
|
||||||
merge(
|
merge(
|
||||||
this.syncWhenDependencyInstalls(),
|
this.syncWhenDependencyInstalls(),
|
||||||
|
// new lan info from sync daemon
|
||||||
combineLatest([app.lanEnabled, this.$lanConnected$, app.status, this.$testingLanConnection$]).pipe(
|
combineLatest([app.lanEnabled, this.$lanConnected$, app.status, this.$testingLanConnection$]).pipe(
|
||||||
filter(([_, __, s, alreadyConnecting]) => s === AppStatus.RUNNING && !alreadyConnecting),
|
filter(([_, __, s, alreadyConnecting]) => s === AppStatus.RUNNING && !alreadyConnecting),
|
||||||
concatMap(([enabled, connected]) => {
|
concatMap(([enabled, connected]) => {
|
||||||
@@ -87,6 +88,22 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
return of()
|
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
|
), //must be final in stack
|
||||||
catchError(e => this.setError(e)),
|
catchError(e => this.setError(e)),
|
||||||
@@ -121,22 +138,6 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
$lanToggled$ = new Subject()
|
$lanToggled$ = new Subject()
|
||||||
ionViewDidEnter () {
|
ionViewDidEnter () {
|
||||||
markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())
|
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) {
|
async doRefresh (event: any) {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ const routes: Routes = [
|
|||||||
path: 'installed/:appId/metrics',
|
path: 'installed/:appId/metrics',
|
||||||
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
|
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({
|
@NgModule({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Rules } from '../../models/app-model'
|
import { Rules } from '../../models/app-model'
|
||||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types'
|
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||||
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull } from './api-types'
|
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull } from './api-types'
|
||||||
@@ -51,7 +51,7 @@ export abstract class ApiService {
|
|||||||
abstract restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit>
|
abstract restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit>
|
||||||
abstract stopAppBackup (appId: string): Promise<Unit>
|
abstract stopAppBackup (appId: string): Promise<Unit>
|
||||||
abstract patchAppConfig (app: AppInstalledPreview, config: object, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }>
|
abstract patchAppConfig (app: AppInstalledPreview, config: object, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }>
|
||||||
abstract postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise< {config: object, breakages: DependentBreakage[] }>
|
abstract postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise< { config: object, breakages: DependentBreakage[] }>
|
||||||
abstract patchServerConfig (attr: string, value: any): Promise<Unit>
|
abstract patchServerConfig (attr: string, value: any): Promise<Unit>
|
||||||
abstract wipeAppData (app: AppInstalledPreview): Promise<Unit>
|
abstract wipeAppData (app: AppInstalledPreview): Promise<Unit>
|
||||||
abstract addSSHKey (sshKey: string): Promise<Unit>
|
abstract addSSHKey (sshKey: string): Promise<Unit>
|
||||||
@@ -62,12 +62,30 @@ export abstract class ApiService {
|
|||||||
abstract restartServer (): Promise<Unit>
|
abstract restartServer (): Promise<Unit>
|
||||||
abstract shutdownServer (): Promise<Unit>
|
abstract shutdownServer (): Promise<Unit>
|
||||||
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
|
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
|
||||||
|
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result}): arg is { error: Error } {
|
||||||
|
return !!(arg as any).error
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRpcSuccess<Error, Result> (arg: { error: Error } | { result: Result}): arg is { result: Result } {
|
||||||
|
return !!(arg as any).result
|
||||||
}
|
}
|
||||||
|
|
||||||
export module ReqRes {
|
export module ReqRes {
|
||||||
export type GetVersionRes = { version: string }
|
export type GetVersionRes = { version: string }
|
||||||
export type PostLoginReq = { password: string }
|
export type PostLoginReq = { password: string }
|
||||||
export type PostLoginRes = Unit
|
export type PostLoginRes = Unit
|
||||||
|
export type ServiceActionRequest = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: string,
|
||||||
|
method: string
|
||||||
|
}
|
||||||
|
export type ServiceActionResponse = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: string
|
||||||
|
} & ({ error: { code: number, message: string } } | { result : string })
|
||||||
export type GetCheckAuthRes = { }
|
export type GetCheckAuthRes = { }
|
||||||
export type GetServerRes = ApiServer
|
export type GetServerRes = ApiServer
|
||||||
export type GetVersionLatestRes = { versionLatest: string, releaseNotes: string }
|
export type GetVersionLatestRes = { versionLatest: string, releaseNotes: string }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { HttpService, Method, HttpOptions } from '../http.service'
|
import { HttpService, Method, HttpOptions } from '../http.service'
|
||||||
import { AppModel, AppStatus } from '../../models/app-model'
|
import { AppModel, AppStatus } from '../../models/app-model'
|
||||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types'
|
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||||
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
||||||
import { ApiService, ReqRes } from './api.service'
|
import { ApiService, ReqRes } from './api.service'
|
||||||
import { ApiServer, Unit } from './api-types'
|
import { ApiServer, Unit } from './api-types'
|
||||||
@@ -12,6 +12,7 @@ import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util'
|
|||||||
import { modulateTime } from 'src/app/util/misc.util'
|
import { modulateTime } from 'src/app/util/misc.util'
|
||||||
import { Observable, of, throwError } from 'rxjs'
|
import { Observable, of, throwError } from 'rxjs'
|
||||||
import { catchError, mapTo } from 'rxjs/operators'
|
import { catchError, mapTo } from 'rxjs/operators'
|
||||||
|
import * as uuid from 'uuid'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveApiService extends ApiService {
|
export class LiveApiService extends ApiService {
|
||||||
@@ -266,6 +267,15 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
||||||
|
const data: ReqRes.ServiceActionRequest = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: uuid.v4(),
|
||||||
|
method: s.id,
|
||||||
|
}
|
||||||
|
return this.authRequest({ method: Method.POST, url: `apps/${appId}/actions`, data })
|
||||||
|
}
|
||||||
|
|
||||||
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
||||||
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
|
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { AppStatus, AppModel } from '../../models/app-model'
|
import { AppStatus, AppModel } from '../../models/app-model'
|
||||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types'
|
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||||
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
|
||||||
import { pauseFor } from '../../util/misc.util'
|
import { pauseFor } from '../../util/misc.util'
|
||||||
import { ApiService, ReqRes } from './api.service'
|
import { ApiService, ReqRes } from './api.service'
|
||||||
@@ -228,6 +228,20 @@ export class MockApiService extends ApiService {
|
|||||||
async shutdownServer (): Promise<EmptyResponse> {
|
async shutdownServer (): Promise<EmptyResponse> {
|
||||||
return mockShutdownServer()
|
return mockShutdownServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async serviceAction (appId: string, action: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
||||||
|
console.log('service action', appId, action)
|
||||||
|
await pauseFor(1000)
|
||||||
|
return {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: '0',
|
||||||
|
// result: 'Congrats! you did ' + action.name,
|
||||||
|
error: {
|
||||||
|
code: 1,
|
||||||
|
message: 'woooo that was bad bad bad',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mockGetServer (): Promise<ReqRes.GetServerRes> {
|
async function mockGetServer (): Promise<ReqRes.GetServerRes> {
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ export const bitcoinI: AppInstalledFull = {
|
|||||||
configuredRequirements: [],
|
configuredRequirements: [],
|
||||||
hasFetchedFull: true,
|
hasFetchedFull: true,
|
||||||
ui: false,
|
ui: false,
|
||||||
restoreAlert: 'if you restore this app horrible things will happen to the people you love.'
|
restoreAlert: 'if you restore this app horrible things will happen to the people you love.',
|
||||||
|
actions: [
|
||||||
|
{ id: 'sync-chain', name: 'Sync Chain', description: 'this will sync with the chain like from Avatar', allowedStatuses: [ AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING ]},
|
||||||
|
{ id: 'off-sync-chain', name: 'Off Sync Chain', description: 'this will off sync with the chain like from Avatar', allowedStatuses: [ AppStatus.STOPPED ]}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lightningI: AppInstalledFull = {
|
export const lightningI: AppInstalledFull = {
|
||||||
@@ -86,6 +90,7 @@ export const lightningI: AppInstalledFull = {
|
|||||||
],
|
],
|
||||||
hasFetchedFull: true,
|
hasFetchedFull: true,
|
||||||
ui: true,
|
ui: true,
|
||||||
|
actions: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cupsI: AppInstalledFull = {
|
export const cupsI: AppInstalledFull = {
|
||||||
@@ -132,6 +137,7 @@ export const cupsI: AppInstalledFull = {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
hasFetchedFull: true,
|
hasFetchedFull: true,
|
||||||
|
actions: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bitcoinA: AppAvailableFull = {
|
export const bitcoinA: AppAvailableFull = {
|
||||||
|
|||||||
@@ -221,11 +221,17 @@ ion-popover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-error-message {
|
.alert-error-message {
|
||||||
.alert-message {
|
.alert-title {
|
||||||
color: var(--ion-color-danger);
|
color: var(--ion-color-danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-success-message {
|
||||||
|
.alert-title {
|
||||||
|
color: var(--ion-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ion-slides {
|
ion-slides {
|
||||||
.slider-wrapper {
|
.slider-wrapper {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user