mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
0.3.0 refactor
ui: adds overlay layer to patch-db-client ui: getting towards mocks ui: cleans up factory init ui: nice type hack ui: live api for patch ui: api service source + http starts up ui: api source + http ui: rework patchdb config, pass stashTimeout into patchDbModel wires in temp patching into api service ui: example of wiring patchdbmodel into page begin integration remove unnecessary method linting first data rendering rework app initialization http source working for ssh delete call temp patches working entire Embassy tab complete not in kansas anymore ripping, saving progress progress for API request response types and endoint defs Update data-model.ts shambles, but in a good way progress big progress progress installed list working big progress progress progress begin marketplace redesign Update api-types.ts Update api-types.ts marketplace improvements cosmetic dependencies and recommendations begin nym auth approach install wizard restore flow and donations
This commit is contained in:
committed by
Aiden McClelland
parent
fd685ae32c
commit
594d93eb3b
22150
ui/package-lock.json
generated
22150
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,29 +21,26 @@
|
|||||||
"@angular/platform-browser": "^11.0.0",
|
"@angular/platform-browser": "^11.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^11.0.0",
|
"@angular/platform-browser-dynamic": "^11.0.0",
|
||||||
"@angular/router": "^11.0.0",
|
"@angular/router": "^11.0.0",
|
||||||
"@ionic/angular": "^5.4.0",
|
"@ionic/angular": "^5.6.0",
|
||||||
"@ionic/storage": "2.2.0",
|
"@ionic/storage": "^3.0.0",
|
||||||
"@start9labs/emver": "^0.1.1",
|
"@ionic/storage-angular": "^3.0.0",
|
||||||
|
"@ngrx/component": "^11.1.1",
|
||||||
|
"@start9labs/emver": "^0.1.4",
|
||||||
"ajv": "^6.12.6",
|
"ajv": "^6.12.6",
|
||||||
"angularx-qrcode": "^10.0.11",
|
"angularx-qrcode": "^11.0.0",
|
||||||
"base32.js": "^0.1.0",
|
|
||||||
"base64url": "^3.0.1",
|
|
||||||
"bip39": "^3.0.2",
|
|
||||||
"bitcoinjs-lib": "^5.2.0",
|
|
||||||
"compare-versions": "^3.5.0",
|
"compare-versions": "^3.5.0",
|
||||||
"core-js": "^3.4.0",
|
"core-js": "^3.4.0",
|
||||||
"handlebars": "^4.7.6",
|
"handlebars": "^4.7.6",
|
||||||
"json-pointer": "^0.6.1",
|
"json-pointer": "^0.6.1",
|
||||||
"jsonpointerx": "^1.0.30",
|
"jsonpointerx": "^1.0.30",
|
||||||
"jsontokens": "^3.0.0",
|
|
||||||
"marked": "^2.0.0",
|
"marked": "^2.0.0",
|
||||||
"rxjs": "^6.6.3",
|
"patch-db-client": "file: ../../../../patch-db-client",
|
||||||
"uuid": "^8.3.1",
|
"rxjs": "^6.6.0",
|
||||||
"zone.js": "^0.11.2",
|
"uuid": "^8.3.0",
|
||||||
"patch-db-client": "file: ../../../../patch-db-client"
|
"zone.js": "^0.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^0.1100.0",
|
"@angular-devkit/build-angular": "^0.1102.0",
|
||||||
"@angular/cli": "^11.0.0",
|
"@angular/cli": "^11.0.0",
|
||||||
"@angular/compiler": "^11.0.0",
|
"@angular/compiler": "^11.0.0",
|
||||||
"@angular/compiler-cli": "^11.0.0",
|
"@angular/compiler-cli": "^11.0.0",
|
||||||
@@ -51,12 +48,12 @@
|
|||||||
"@ionic/angular-toolkit": "^3.0.0",
|
"@ionic/angular-toolkit": "^3.0.0",
|
||||||
"@ionic/lab": "^3.2.9",
|
"@ionic/lab": "^3.2.9",
|
||||||
"@types/json-pointer": "^1.0.30",
|
"@types/json-pointer": "^1.0.30",
|
||||||
"@types/marked": "^1.1.0",
|
"@types/marked": "^2.0.0",
|
||||||
"@types/node": "^14.11.10",
|
"@types/node": "^15.0.0",
|
||||||
"@types/uuid": "^8.0.0",
|
"@types/uuid": "^8.0.0",
|
||||||
"node-html-parser": "2.0.0",
|
"node-html-parser": "^3.2.0",
|
||||||
"ts-node": "^9.1.0",
|
"ts-node": "^9.1.0",
|
||||||
"tslint": "^6.1.0",
|
"tslint": "^6.1.0",
|
||||||
"typescript": "4.0.5"
|
"typescript": "4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { NgModule } from '@angular/core'
|
|||||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||||
import { AuthGuard } from './guards/auth.guard'
|
import { AuthGuard } from './guards/auth.guard'
|
||||||
import { UnauthGuard } from './guards/unauth.guard'
|
import { UnauthGuard } from './guards/unauth.guard'
|
||||||
|
import { MaintenanceGuard } from './guards/maintenance.guard'
|
||||||
|
import { UnmaintenanceGuard } from './guards/unmaintenance.guard'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -10,34 +12,32 @@ const routes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'authenticate',
|
path: 'auth',
|
||||||
canActivate: [UnauthGuard],
|
canActivate: [UnauthGuard],
|
||||||
pathMatch: 'full',
|
loadChildren: () => import('./pages/auth-routes/auth-routing.module').then(m => m.AuthRoutingModule),
|
||||||
loadChildren: () => import('./pages/authenticate/authenticate.module').then( m => m.AuthenticatePageModule),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'embassy',
|
path: 'embassy',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard, MaintenanceGuard],
|
||||||
canActivateChild: [AuthGuard],
|
canActivateChild: [AuthGuard, MaintenanceGuard],
|
||||||
loadChildren: () => import('./pages/server-routes/server-routing.module').then(m => m.ServerRoutingModule),
|
loadChildren: () => import('./pages/server-routes/server-routing.module').then(m => m.ServerRoutingModule),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'maintenance',
|
||||||
|
canActivate: [AuthGuard, UnmaintenanceGuard],
|
||||||
|
loadChildren: () => import('./pages/maintenance/maintenance.module').then(m => m.MaintenancePageModule),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'notifications',
|
path: 'notifications',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard, MaintenanceGuard],
|
||||||
canActivateChild: [AuthGuard],
|
|
||||||
loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule),
|
loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'services',
|
path: 'services',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard, MaintenanceGuard],
|
||||||
canActivateChild: [AuthGuard],
|
canActivateChild: [AuthGuard, MaintenanceGuard],
|
||||||
loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule),
|
loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule),
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: 'drives',
|
|
||||||
// canActivate: [AuthGuard],
|
|
||||||
// loadChildren: () => import('./pages/server-routes/external-drives/external-drives.module').then( m => m.ExternalDrivesPageModule),
|
|
||||||
// },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<ion-app>
|
<ion-app *ngIf="patch.initialized">
|
||||||
<ion-split-pane (ionSplitPaneVisible)="splitPaneVisible($event)" contentId="main-content">
|
<ion-split-pane [disabled]="!showMenu" (ionSplitPaneVisible)="splitPaneVisible($event)" contentId="main-content">
|
||||||
<ion-menu contentId="main-content" type="overlay">
|
<ion-menu contentId="main-content" type="overlay">
|
||||||
<ion-header *ngIf="$showMenuContent$ | async" class="menu-style">
|
<ion-header>
|
||||||
<ion-toolbar style="--background: var(--ion-background-color);">
|
<ion-toolbar style="--background: var(--ion-background-color);">
|
||||||
<ion-title *ngIf="serverName$ | async as name">{{ name }}</ion-title>
|
<ion-title *ngIf="patch.watch$('ui', 'server-name') | ngrxPush as name; else dots">{{ name }}</ion-title>
|
||||||
<ion-title *ngIf="!(serverName$ | async)"><ion-spinner name="dots" color="warning"></ion-spinner></ion-title>
|
<ng-template #dots>
|
||||||
|
<ion-title><ion-spinner name="dots" color="warning"></ion-spinner></ion-title>
|
||||||
|
</ng-template>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content scroll-y="false" class="menu-style">
|
<ion-content scroll-y="false" class="menu-style">
|
||||||
<ng-container *ngIf="$showMenuContent$ | async">
|
<ng-container>
|
||||||
<ion-list style="padding: 0px">
|
<ion-list style="padding: 0px">
|
||||||
<ion-menu-toggle auto-hide="false" *ngFor="let page of appPages; let i = index">
|
<ion-menu-toggle auto-hide="false" *ngFor="let page of appPages; let i = index">
|
||||||
<ion-item
|
<ion-item
|
||||||
@@ -18,36 +20,24 @@
|
|||||||
[routerLink]="[page.url]"
|
[routerLink]="[page.url]"
|
||||||
lines="none"
|
lines="none"
|
||||||
detail="false"
|
detail="false"
|
||||||
[class.selected]="selectedIndex == i"
|
[class.selected]="selectedIndex === i"
|
||||||
>
|
>
|
||||||
<ion-icon slot="start" [name]="page.icon"></ion-icon>
|
<ion-icon slot="start" [name]="page.icon"></ion-icon>
|
||||||
<ion-label style="font-family: 'Montserrat';">{{ page.title }}</ion-label>
|
<ion-label style="font-family: 'Montserrat';">{{ page.title }}</ion-label>
|
||||||
<ion-badge *ngIf="page.url === '/notifications' && (serverBadge$ | async) as s" color="danger" style="margin-right: 3%;" [class.selected-badge]="selectedIndex == i">{{s}}</ion-badge>
|
<ion-badge *ngIf="page.url === '/notifications' && (patch.watch$('server-info', 'unread-notification-count') | ngrxPush) as badge" color="danger" style="margin-right: 3%;" [class.selected-badge]="selectedIndex == i">{{ badge }}</ion-badge>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-menu-toggle>
|
</ion-menu-toggle>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!($showMenuContent$ | async)">
|
|
||||||
<ion-card>
|
|
||||||
<ion-card-header>
|
|
||||||
<ion-card-title>Welcome</ion-card-title>
|
|
||||||
</ion-card-header>
|
|
||||||
<ion-card-content>
|
|
||||||
<p>This is the private website of your Start9 Embassy device.</p>
|
|
||||||
<br />
|
|
||||||
<p>Please authenticate yourself to continue.</p>
|
|
||||||
</ion-card-content>
|
|
||||||
</ion-card>
|
|
||||||
</ng-container>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
<ion-footer style="padding-bottom: 5%; text-align: center;" class="menu-style">
|
<ion-footer style="padding-bottom: 16px; text-align: center;" class="menu-style">
|
||||||
<ion-menu-toggle auto-hide="false">
|
<ion-menu-toggle auto-hide="false">
|
||||||
<ion-item *ngIf="$showMenuContent$ | async" button style="--background:var(--ion-background-color); margin-bottom: 10%;" fill="clear" (click)="presentAlertLogout()">
|
<ion-item button style="--background:var(--ion-background-color); margin-bottom: 16px;" fill="clear" (click)="presentAlertLogout()">
|
||||||
<ion-icon size="small" slot="start" color="dark" name="log-out-outline"></ion-icon>
|
<ion-icon size="small" slot="start" color="dark" name="log-out-outline"></ion-icon>
|
||||||
<ion-label><ion-text color="danger">Logout</ion-text></ion-label>
|
<ion-label><ion-text color="danger">Logout</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-menu-toggle>
|
</ion-menu-toggle>
|
||||||
<img style="width: 36%;" src="assets/logo-full.png">
|
<img style="width: 25%;" src="assets/img/logo.png">
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
</ion-menu>
|
</ion-menu>
|
||||||
<ion-router-outlet id="main-content"></ion-router-outlet>
|
<ion-router-outlet id="main-content"></ion-router-outlet>
|
||||||
@@ -66,12 +56,13 @@
|
|||||||
<ion-icon name="add"></ion-icon>
|
<ion-icon name="add"></ion-icon>
|
||||||
<ion-icon name="alert-outline"></ion-icon>
|
<ion-icon name="alert-outline"></ion-icon>
|
||||||
<ion-icon name="alert-circle-outline"></ion-icon>
|
<ion-icon name="alert-circle-outline"></ion-icon>
|
||||||
|
<ion-icon name="aperture-outline"></ion-icon>
|
||||||
<ion-icon name="arrow-back"></ion-icon>
|
<ion-icon name="arrow-back"></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="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="chevron-forward"></ion-icon> <!-- needed for detail="true" on ion-item button -->
|
||||||
<ion-icon name="close"></ion-icon>
|
<ion-icon name="close"></ion-icon>
|
||||||
<ion-icon name="close-outline"></ion-icon>
|
<ion-icon name="close-outline"></ion-icon>
|
||||||
<ion-icon name="code-outline"></ion-icon>
|
<ion-icon name="code-outline"></ion-icon>
|
||||||
@@ -130,7 +121,6 @@
|
|||||||
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
|
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
<ion-input></ion-input>
|
<ion-input></ion-input>
|
||||||
<ion-input *ngIf="untilLoaded" type="password" value="getdots"></ion-input>
|
|
||||||
<ion-item></ion-item>
|
<ion-item></ion-item>
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
<ion-item-group></ion-item-group>
|
<ion-item-group></ion-item-group>
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ServerModel, ServerStatus } from './models/server-model'
|
|
||||||
import { Storage } from '@ionic/storage'
|
import { Storage } from '@ionic/storage'
|
||||||
import { SyncDaemon } from './services/sync.service'
|
|
||||||
import { AuthService, AuthState } from './services/auth.service'
|
import { AuthService, AuthState } from './services/auth.service'
|
||||||
import { ApiService } from './services/api/api.service'
|
import { ApiService } from './services/api/api.service'
|
||||||
import { Router } from '@angular/router'
|
import { Router, RoutesRecognized } from '@angular/router'
|
||||||
import { BehaviorSubject, Observable } from 'rxjs'
|
import { distinctUntilChanged, filter, finalize, takeWhile } from 'rxjs/operators'
|
||||||
import { AppModel } from './models/app-model'
|
import { AlertController, ToastController } from '@ionic/angular'
|
||||||
import { filter, take } from 'rxjs/operators'
|
|
||||||
import { AlertController } from '@ionic/angular'
|
|
||||||
import { LoaderService } from './services/loader.service'
|
import { LoaderService } from './services/loader.service'
|
||||||
import { Emver } from './services/emver.service'
|
import { Emver } from './services/emver.service'
|
||||||
import { SplitPaneTracker } from './services/split-pane.service'
|
import { SplitPaneTracker } from './services/split-pane.service'
|
||||||
import { LoadingOptions } from '@ionic/core'
|
import { LoadingOptions } from '@ionic/core'
|
||||||
import { pauseFor } from './util/misc.util'
|
import { PatchDbModel } from './models/patch-db/patch-db-model'
|
||||||
|
import { HttpService } from './services/http.service'
|
||||||
|
import { ServerStatus } from './models/patch-db/data-model'
|
||||||
|
import { ConnectionService } from './services/connection.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -21,11 +20,8 @@ import { pauseFor } from './util/misc.util'
|
|||||||
styleUrls: ['app.component.scss'],
|
styleUrls: ['app.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
isUpdating = false
|
ServerStatus = ServerStatus
|
||||||
fullPageMenu = true
|
showMenu = false
|
||||||
$showMenuContent$ = new BehaviorSubject(false)
|
|
||||||
serverName$ : Observable<string>
|
|
||||||
serverBadge$: Observable<number>
|
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
untilLoaded = true
|
untilLoaded = true
|
||||||
appPages = [
|
appPages = [
|
||||||
@@ -49,92 +45,118 @@ export class AppComponent {
|
|||||||
url: '/notifications',
|
url: '/notifications',
|
||||||
icon: 'notifications-outline',
|
icon: 'notifications-outline',
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// title: 'Backup drives',
|
|
||||||
// url: '/drives',
|
|
||||||
// icon: 'albums-outline',
|
|
||||||
// },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly serverModel: ServerModel,
|
|
||||||
private readonly syncDaemon: SyncDaemon,
|
|
||||||
private readonly storage: Storage,
|
private readonly storage: Storage,
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly router: Router,
|
private readonly router: Router,
|
||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
|
private readonly http: HttpService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly loader: LoaderService,
|
private readonly loader: LoaderService,
|
||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
|
private readonly connectionService: ConnectionService,
|
||||||
|
private readonly toastCtrl: ToastController,
|
||||||
readonly splitPane: SplitPaneTracker,
|
readonly splitPane: SplitPaneTracker,
|
||||||
|
readonly patch: PatchDbModel,
|
||||||
) {
|
) {
|
||||||
// set dark theme
|
// set dark theme
|
||||||
document.body.classList.toggle('dark', true)
|
document.body.classList.toggle('dark', true)
|
||||||
this.serverName$ = this.serverModel.watch().name
|
|
||||||
this.serverBadge$ = this.serverModel.watch().badge
|
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidEnter () {
|
|
||||||
// weird bug where a browser grabbed the value 'getdots' from the app.component.html preload input field.
|
|
||||||
// this removes that field after prleloading occurs.
|
|
||||||
pauseFor(500).then(() => this.untilLoaded = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async init () {
|
async init () {
|
||||||
let fromFresh = true
|
await this.storage.create()
|
||||||
await this.storage.ready()
|
await this.patch.init()
|
||||||
await this.authService.restoreCache()
|
await this.authService.init()
|
||||||
await this.emver.init()
|
await this.emver.init()
|
||||||
|
|
||||||
this.authService.listen({
|
|
||||||
[AuthState.VERIFIED]: async () => {
|
|
||||||
console.log('verified')
|
|
||||||
this.api.authenticatedRequestsEnabled = true
|
|
||||||
await this.serverModel.restoreCache()
|
|
||||||
await this.appModel.restoreCache()
|
|
||||||
this.syncDaemon.start()
|
|
||||||
this.$showMenuContent$.next(true)
|
|
||||||
if (fromFresh) {
|
|
||||||
this.router.initialNavigation()
|
this.router.initialNavigation()
|
||||||
fromFresh = false
|
|
||||||
}
|
// watch auth
|
||||||
},
|
this.authService.watch$()
|
||||||
[AuthState.UNVERIFIED]: () => {
|
.subscribe(auth => {
|
||||||
console.log('unverified')
|
// VERIFIED
|
||||||
this.api.authenticatedRequestsEnabled = false
|
if (auth === AuthState.VERIFIED) {
|
||||||
this.serverModel.clear()
|
this.http.authReqEnabled = true
|
||||||
this.appModel.clear()
|
this.showMenu = true
|
||||||
this.syncDaemon.stop()
|
this.patch.start()
|
||||||
|
// watch network
|
||||||
|
this.watchNetwork(auth)
|
||||||
|
// watch router to highlight selected menu item
|
||||||
|
this.watchRouter(auth)
|
||||||
|
// watch status to display/hide maintenance page
|
||||||
|
this.watchStatus(auth)
|
||||||
|
// watch unread notification count to display toast
|
||||||
|
this.watchNotifications(auth)
|
||||||
|
// UNVERIFIED
|
||||||
|
} else if (auth === AuthState.UNVERIFIED) {
|
||||||
|
this.http.authReqEnabled = false
|
||||||
|
this.showMenu = false
|
||||||
|
this.patch.stop()
|
||||||
this.storage.clear()
|
this.storage.clear()
|
||||||
this.router.navigate(['/authenticate'], { replaceUrl: true })
|
this.router.navigate(['/auth'], { replaceUrl: true })
|
||||||
this.$showMenuContent$.next(false)
|
|
||||||
if (fromFresh) {
|
|
||||||
this.router.initialNavigation()
|
|
||||||
fromFresh = false
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.serverModel.watch().status.subscribe(s => {
|
this.http.watch401$().subscribe(() => {
|
||||||
this.isUpdating = (s === ServerStatus.UPDATING)
|
this.authService.setUnverified()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.router.events.pipe(filter(e => !!(e as any).urlAfterRedirects)).subscribe((e: any) => {
|
private watchNetwork (auth: AuthState): void {
|
||||||
|
this.connectionService.monitor$()
|
||||||
|
.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
takeWhile(() => auth === AuthState.VERIFIED),
|
||||||
|
)
|
||||||
|
.subscribe(c => {
|
||||||
|
console.log('CONNECTION CHANGED', c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private watchRouter (auth: AuthState): void {
|
||||||
|
this.router.events
|
||||||
|
.pipe(
|
||||||
|
filter((e: RoutesRecognized) => !!e.urlAfterRedirects),
|
||||||
|
takeWhile(() => auth === AuthState.VERIFIED),
|
||||||
|
)
|
||||||
|
.subscribe(e => {
|
||||||
const appPageIndex = this.appPages.findIndex(
|
const appPageIndex = this.appPages.findIndex(
|
||||||
appPage => (e.urlAfterRedirects || e.url || '').startsWith(appPage.url),
|
appPage => e.urlAfterRedirects.startsWith(appPage.url),
|
||||||
)
|
)
|
||||||
if (appPageIndex > -1) this.selectedIndex = appPageIndex
|
if (appPageIndex > -1) this.selectedIndex = appPageIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: while this works, it is dangerous and impractical.
|
private watchStatus (auth: AuthState): void {
|
||||||
if (e.urlAfterRedirects !== '/embassy' && e.urlAfterRedirects !== '/authenticate' && this.isUpdating) {
|
this.patch.watch$('server-info', 'status')
|
||||||
this.router.navigateByUrl('/embassy')
|
.pipe(
|
||||||
|
takeWhile(() => auth === AuthState.VERIFIED),
|
||||||
|
)
|
||||||
|
.subscribe(status => {
|
||||||
|
const maintenance = '/maintenance'
|
||||||
|
const url = this.router.url
|
||||||
|
if (status === ServerStatus.Running && url.startsWith(maintenance)) {
|
||||||
|
this.router.navigate([''], { replaceUrl: true })
|
||||||
|
}
|
||||||
|
if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(status) && !url.startsWith(maintenance)) {
|
||||||
|
this.router.navigate([maintenance], { replaceUrl: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.api.watch401$().subscribe(() => {
|
}
|
||||||
this.authService.setAuthStateUnverified()
|
|
||||||
return this.api.postLogout()
|
private watchNotifications (auth: AuthState): void {
|
||||||
|
let previous: number
|
||||||
|
this.patch.watch$('server-info', 'unread-notification-count')
|
||||||
|
.pipe(
|
||||||
|
takeWhile(() => auth === AuthState.VERIFIED),
|
||||||
|
finalize(() => console.log('FINALIZING!!!')),
|
||||||
|
)
|
||||||
|
.subscribe(count => {
|
||||||
|
if (previous !== undefined && count > previous) this.presentToastNotifications()
|
||||||
|
previous = count
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,20 +183,45 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async logout () {
|
private async logout () {
|
||||||
this.serverName$.pipe(take(1)).subscribe(name => {
|
this.loader.of(LoadingSpinner('Logging out...'))
|
||||||
this.loader.of(LoadingSpinner(`Logging out ${name || ''}...`))
|
.displayDuringP(this.api.logout({ }))
|
||||||
.displayDuringP(this.api.postLogout())
|
.then(() => this.authService.setUnverified())
|
||||||
.then(() => this.authService.setAuthStateUnverified())
|
|
||||||
.catch(e => this.setError(e))
|
.catch(e => this.setError(e))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setError (e: Error) {
|
private async presentToastNotifications () {
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
header: 'Embassy',
|
||||||
|
message: `New notifications`,
|
||||||
|
position: 'bottom',
|
||||||
|
duration: 4000,
|
||||||
|
cssClass: 'notification-toast',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
side: 'start',
|
||||||
|
icon: 'close',
|
||||||
|
handler: () => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
side: 'end',
|
||||||
|
text: 'View',
|
||||||
|
handler: () => {
|
||||||
|
this.router.navigate(['/notifications'])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
await toast.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setError (e: Error) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
await this.presentError(e.message)
|
await this.presentError(e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentError (e: string) {
|
private async presentError (e: string) {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
backdropDismiss: true,
|
backdropDismiss: true,
|
||||||
message: `Exception on logout: ${e}`,
|
message: `Exception on logout: ${e}`,
|
||||||
@@ -189,7 +236,7 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
splitPaneVisible (e: any) {
|
splitPaneVisible (e: any) {
|
||||||
this.splitPane.$menuFixedOpenOnLeft$.next(e.detail.visible)
|
this.splitPane.menuFixedOpenOnLeft$.next(e.detail.visible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,23 @@ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
import { RouteReuseStrategy } from '@angular/router'
|
import { RouteReuseStrategy } from '@angular/router'
|
||||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'
|
||||||
import { IonicStorageModule } from '@ionic/storage'
|
import { Drivers } from '@ionic/storage'
|
||||||
|
import { IonicStorageModule } from '@ionic/storage-angular'
|
||||||
import { HttpClientModule } from '@angular/common/http'
|
import { HttpClientModule } from '@angular/common/http'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { ApiService } from './services/api/api.service'
|
import { ApiService } from './services/api/api.service'
|
||||||
import { ApiServiceFactory } from './services/api/api.service.factory'
|
import { ApiServiceFactory } from './services/api/api.service.factory'
|
||||||
import { AppModel } from './models/app-model'
|
import { PatchDbModelFactory } from './models/patch-db/patch-db-model.factory'
|
||||||
import { HttpService } from './services/http.service'
|
import { HttpService } from './services/http.service'
|
||||||
import { ServerModel } from './models/server-model'
|
|
||||||
import { ConfigService } from './services/config.service'
|
import { ConfigService } from './services/config.service'
|
||||||
import { QRCodeModule } from 'angularx-qrcode'
|
import { QRCodeModule } from 'angularx-qrcode'
|
||||||
import { APP_CONFIG_COMPONENT_MAPPING } from './modals/app-config-injectable/modal-injectable-token'
|
import { APP_CONFIG_COMPONENT_MAPPING } from './modals/app-config-injectable/modal-injectable-token'
|
||||||
import { appConfigComponents } from './modals/app-config-injectable/modal-injectable-value';
|
import { appConfigComponents } from './modals/app-config-injectable/modal-injectable-value'
|
||||||
import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
|
import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
|
||||||
|
import { PatchDbModel } from './models/patch-db/patch-db-model'
|
||||||
|
import { LocalStorageBootstrap } from './models/patch-db/local-storage-bootstrap'
|
||||||
|
import { SharingModule } from './modules/sharing.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
@@ -25,13 +28,21 @@ import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module'
|
|||||||
BrowserModule,
|
BrowserModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
IonicStorageModule.forRoot(),
|
IonicStorageModule.forRoot({
|
||||||
|
storeName: '_embassykv',
|
||||||
|
dbKey: '_embassykey',
|
||||||
|
name: '_embassystorage',
|
||||||
|
driverOrder: [Drivers.LocalStorage, Drivers.IndexedDB],
|
||||||
|
}),
|
||||||
QRCodeModule,
|
QRCodeModule,
|
||||||
OSWelcomePageModule,
|
OSWelcomePageModule,
|
||||||
|
SharingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
Storage,
|
||||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||||
{ provide: ApiService, useFactory: ApiServiceFactory, deps: [ConfigService, HttpService, AppModel, ServerModel] },
|
{ provide: ApiService , useFactory: ApiServiceFactory, deps: [ConfigService, HttpService] },
|
||||||
|
{ provide: PatchDbModel, useFactory: PatchDbModelFactory, deps: [ConfigService, LocalStorageBootstrap, ApiService] },
|
||||||
{ provide: APP_CONFIG_COMPONENT_MAPPING, useValue: appConfigComponents },
|
{ provide: APP_CONFIG_COMPONENT_MAPPING, useValue: appConfigComponents },
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
|
||||||
import { ModalController } from '@ionic/angular'
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
|
||||||
import { DiskPartition } from 'src/app/models/server-model'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-backup-confirmation',
|
|
||||||
templateUrl: './app-backup-confirmation.component.html',
|
|
||||||
styleUrls: ['./app-backup-confirmation.component.scss'],
|
|
||||||
})
|
|
||||||
export class AppBackupConfirmationComponent implements OnInit {
|
|
||||||
unmasked = false
|
|
||||||
password: string
|
|
||||||
$error$: BehaviorSubject<string> = new BehaviorSubject('')
|
|
||||||
|
|
||||||
// TODO: EJECT-DISKS pass this through the modalCtrl once ejecting disks is an option in the UI.
|
|
||||||
eject = true
|
|
||||||
message: string
|
|
||||||
|
|
||||||
@Input() app: AppInstalledFull
|
|
||||||
@Input() partition: DiskPartition
|
|
||||||
|
|
||||||
constructor (private readonly modalCtrl: ModalController) { }
|
|
||||||
ngOnInit () {
|
|
||||||
this.message = `Enter your master password to create an encrypted backup of ${this.app.title} to "${this.partition.label || this.partition.logicalname}".`
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleMask () {
|
|
||||||
this.unmasked = !this.unmasked
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel () {
|
|
||||||
this.modalCtrl.dismiss({ cancel: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
submit () {
|
|
||||||
if (!this.password || this.password.length < 12) {
|
|
||||||
this.$error$.next('Password must be at least 12 characters in length.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { password } = this
|
|
||||||
this.modalCtrl.dismiss({ password })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
<div style="position: relative; margin-right: 1vh;">
|
<div style="position: relative; margin-right: 1vh;">
|
||||||
<ion-icon
|
<ion-badge mode="md" class="md-badge" *ngIf="(badge$ | ngrxPush) && !(menuFixedOpen$ | ngrxPush)" color="danger">{{ badge$ | ngrxPush }}</ion-badge>
|
||||||
*ngIf="(badge$ | async) && !(menuFixedOpen$ | async)"
|
|
||||||
size="medium"
|
|
||||||
color="dark"
|
|
||||||
[class.ios-badge]="isIos"
|
|
||||||
[class.md-badge]="!isIos"
|
|
||||||
name="alert-outline"
|
|
||||||
>
|
|
||||||
</ion-icon>
|
|
||||||
<ion-menu-button color="dark"></ion-menu-button>
|
<ion-menu-button color="dark"></ion-menu-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
.ios-badge {
|
// .ios-badge {
|
||||||
background-color: var(--ion-color-start9);
|
// background-color: var(--ion-color-start9);
|
||||||
position: absolute;
|
// position: absolute;
|
||||||
top: 1px;
|
// top: 1px;
|
||||||
left: 62%;
|
// left: 62%;
|
||||||
border-radius: 5px;
|
// border-radius: 5px;
|
||||||
z-index: 1;
|
// z-index: 1;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.md-badge {
|
.md-badge {
|
||||||
background-color: var(--ion-color-start9);
|
background-color: var(--ion-color-start9);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: -8px;
|
||||||
left: 56%;
|
left: 56%;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ServerModel } from '../../models/server-model'
|
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
|
||||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||||
import { isPlatform } from '@ionic/angular'
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'badge-menu-button',
|
selector: 'badge-menu-button',
|
||||||
@@ -12,16 +10,14 @@ import { isPlatform } from '@ionic/angular'
|
|||||||
})
|
})
|
||||||
|
|
||||||
export class BadgeMenuComponent {
|
export class BadgeMenuComponent {
|
||||||
badge$: Observable<boolean>
|
badge$: Observable<number>
|
||||||
menuFixedOpen$: Observable<boolean>
|
menuFixedOpen$: Observable<boolean>
|
||||||
isIos: boolean
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly serverModel: ServerModel,
|
|
||||||
private readonly splitPane: SplitPaneTracker,
|
private readonly splitPane: SplitPaneTracker,
|
||||||
|
private readonly patch: PatchDbModel,
|
||||||
) {
|
) {
|
||||||
this.menuFixedOpen$ = this.splitPane.$menuFixedOpenOnLeft$.asObservable()
|
this.menuFixedOpen$ = this.splitPane.menuFixedOpenOnLeft$.asObservable()
|
||||||
this.badge$ = this.serverModel.watch().badge.pipe(map(i => i > 0))
|
this.badge$ = this.patch.watch$('server-info', 'unread-notification-count')
|
||||||
this.isIos = isPlatform('ios')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ValueSpec } from 'src/app/app-config/config-types'
|
import { ValueSpec } from 'src/app/pkg-config/config-types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'config-header',
|
selector: 'config-header',
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<div *ngFor="let dep of dependenciesToDisplay; let index = i">
|
|
||||||
<marketplace-dependency-item *ngIf="depType === 'available'" style="width: 100%"
|
|
||||||
[dep]="dep"
|
|
||||||
[hostApp]="hostApp"
|
|
||||||
[$loading$]="$loading$"
|
|
||||||
>
|
|
||||||
</marketplace-dependency-item>
|
|
||||||
<installed-dependency-item *ngIf="depType === 'installed'" style="width: 100%"
|
|
||||||
[dep]="dep"
|
|
||||||
[hostApp]="hostApp"
|
|
||||||
[$loading$]="$loading$"
|
|
||||||
>
|
|
||||||
</installed-dependency-item>
|
|
||||||
</div>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { DependencyListComponent } from './dependency-list.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { RouterModule } from '@angular/router'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
import { InformationPopoverComponentModule } from '../information-popover/information-popover.component.module'
|
|
||||||
import { StatusComponentModule } from '../status/status.component.module'
|
|
||||||
import { InstalledDependencyItemComponentModule } from './installed-dependency-item/installed-dependency-item.component.module'
|
|
||||||
import { MarketplaceDependencyItemComponentModule } from './marketplace-dependency-item/marketplace-dependency-item.component.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
DependencyListComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild([]),
|
|
||||||
SharingModule,
|
|
||||||
InformationPopoverComponentModule,
|
|
||||||
StatusComponentModule,
|
|
||||||
InstalledDependencyItemComponentModule,
|
|
||||||
MarketplaceDependencyItemComponentModule,
|
|
||||||
],
|
|
||||||
exports: [DependencyListComponent],
|
|
||||||
})
|
|
||||||
export class DependencyListComponentModule { }
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import { AppDependency, BaseApp, isOptional } from 'src/app/models/app-types'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'dependency-list',
|
|
||||||
templateUrl: './dependency-list.component.html',
|
|
||||||
styleUrls: ['./dependency-list.component.scss'],
|
|
||||||
})
|
|
||||||
export class DependencyListComponent {
|
|
||||||
@Input() depType: 'installed' | 'available' = 'available'
|
|
||||||
@Input() hostApp: BaseApp
|
|
||||||
@Input() dependencies: AppDependency[]
|
|
||||||
dependenciesToDisplay: AppDependency[]
|
|
||||||
@Input() $loading$: BehaviorSubject<boolean> = new BehaviorSubject(true)
|
|
||||||
|
|
||||||
constructor () { }
|
|
||||||
|
|
||||||
ngOnChanges () {
|
|
||||||
this.dependenciesToDisplay = this.dependencies.filter(dep =>
|
|
||||||
this.depType === 'available' ? !isOptional(dep) : true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.dependenciesToDisplay = this.dependencies.filter(dep =>
|
|
||||||
this.depType === 'available' ? !isOptional(dep) : true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="{ loading: $loading$ | async, disabled: installing || ($loading$ | async) } as l" >
|
|
||||||
<ion-item
|
|
||||||
class="dependency"
|
|
||||||
lines="none"
|
|
||||||
>
|
|
||||||
<ion-avatar style="position: relative; height: 5vh; width: 5vh; margin: 0px;" slot="start">
|
|
||||||
<div *ngIf="!l.loading" class="badge" [style]="badgeStyle"></div>
|
|
||||||
<img [src]="dep.iconURL | iconParse" />
|
|
||||||
</ion-avatar>
|
|
||||||
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
|
|
||||||
<h4 style="font-family: 'Montserrat'">{{ dep.title }}</h4>
|
|
||||||
<p style="font-size: small">{{ dep.versionSpec }}</p>
|
|
||||||
<p *ngIf="!l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="color">{{statusText}}</ion-text></p>
|
|
||||||
<p *ngIf="l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text color="medium">Refreshing</ion-text></p>
|
|
||||||
</ion-label>
|
|
||||||
|
|
||||||
<ion-button size="small" (click)="action()" *ngIf="!installing && !l.loading" color="primary" fill="outline" style="font-size: x-small">
|
|
||||||
{{actionText}}
|
|
||||||
</ion-button>
|
|
||||||
|
|
||||||
<div slot="end" *ngIf='installing || (l.loading)' >
|
|
||||||
<div *ngIf='installing && !(l.loading)' class="spinner">
|
|
||||||
<ion-spinner [color]="color" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf='(l.loading)' class="spinner">
|
|
||||||
<ion-spinner [color]="medium" style="height: 3vh; width: 3vh" name="lines"></ion-spinner>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { InstalledDependencyItemComponent } from './installed-dependency-item.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { RouterModule } from '@angular/router'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module'
|
|
||||||
import { StatusComponentModule } from '../../status/status.component.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [InstalledDependencyItemComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild([]),
|
|
||||||
SharingModule,
|
|
||||||
InformationPopoverComponentModule,
|
|
||||||
StatusComponentModule,
|
|
||||||
],
|
|
||||||
exports: [InstalledDependencyItemComponent],
|
|
||||||
})
|
|
||||||
export class InstalledDependencyItemComponentModule { }
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
|
|
||||||
.spinner {
|
|
||||||
background: rgba(0,0,0,0);
|
|
||||||
border-radius: 100px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: absolute; width: 2.5vh;
|
|
||||||
height: 2.5vh;
|
|
||||||
border-radius: 50px;
|
|
||||||
left: -1vh;
|
|
||||||
top: -1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xSmallText {
|
|
||||||
font-size: x-small !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mediumText {
|
|
||||||
font-size: medium !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opacityUp {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
|
||||||
import { NavigationExtras } from '@angular/router'
|
|
||||||
import { AlertController, NavController } from '@ionic/angular'
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs'
|
|
||||||
import { AppStatus } from 'src/app/models/app-model'
|
|
||||||
import { AppDependency, BaseApp, DependencyViolationSeverity, getInstalledViolationSeverity, getViolationSeverity, isInstalling, isMisconfigured, isMissing, isNotRunning, isVersionMismatch } from 'src/app/models/app-types'
|
|
||||||
import { Recommendation } from '../../recommendation-button/recommendation-button.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'installed-dependency-item',
|
|
||||||
templateUrl: './installed-dependency-item.component.html',
|
|
||||||
styleUrls: ['./installed-dependency-item.component.scss'],
|
|
||||||
})
|
|
||||||
export class InstalledDependencyItemComponent implements OnInit {
|
|
||||||
@Input() dep: AppDependency
|
|
||||||
@Input() hostApp: BaseApp
|
|
||||||
@Input() $loading$: BehaviorSubject<boolean>
|
|
||||||
|
|
||||||
isLoading$: Observable<boolean>
|
|
||||||
color: string
|
|
||||||
installing = false
|
|
||||||
badgeStyle: string
|
|
||||||
violationSeverity: DependencyViolationSeverity
|
|
||||||
statusText: string
|
|
||||||
actionText: string
|
|
||||||
action: () => Promise<any>
|
|
||||||
|
|
||||||
constructor (private readonly navCtrl: NavController, private readonly alertCtrl: AlertController) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.violationSeverity = getInstalledViolationSeverity(this.dep)
|
|
||||||
|
|
||||||
const { color, statusText, installing, actionText, action } = this.getValues()
|
|
||||||
|
|
||||||
this.color = color
|
|
||||||
this.statusText = statusText
|
|
||||||
this.installing = installing
|
|
||||||
this.actionText = actionText
|
|
||||||
this.action = action
|
|
||||||
this.badgeStyle = `background: radial-gradient(var(--ion-color-${this.color}) 40%, transparent)`
|
|
||||||
}
|
|
||||||
|
|
||||||
isDanger () {
|
|
||||||
// installed dep violations are either REQUIRED or NONE, by getInstalledViolationSeverity above.
|
|
||||||
return [DependencyViolationSeverity.REQUIRED].includes(this.violationSeverity)
|
|
||||||
}
|
|
||||||
|
|
||||||
getValues (): { color: string, statusText: string, installing: boolean, actionText: string, action: () => Promise<any> } {
|
|
||||||
if (isInstalling(this.dep)) return { color: 'primary', statusText: 'Installing', installing: true, actionText: undefined, action: () => this.view() }
|
|
||||||
if (!this.isDanger()) return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View', action: () => this.view() }
|
|
||||||
|
|
||||||
if (isMissing(this.dep)) return { color: 'warning', statusText: 'Not Installed', installing: false, actionText: 'Install', action: () => this.install() }
|
|
||||||
if (isVersionMismatch(this.dep)) return { color: 'warning', statusText: 'Incompatible Version Installed', installing: false, actionText: 'Update', action: () => this.install() }
|
|
||||||
if (isMisconfigured(this.dep)) return { color: 'warning', statusText: 'Incompatible Config', installing: false, actionText: 'Configure', action: () => this.configure() }
|
|
||||||
if (isNotRunning(this.dep)) return { color: 'warning', statusText: 'Not Running', installing: false, actionText: 'View', action: () => this.view() }
|
|
||||||
return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View', action: () => this.view() }
|
|
||||||
}
|
|
||||||
|
|
||||||
async view () {
|
|
||||||
return this.navCtrl.navigateForward(`/services/installed/${this.dep.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async install () {
|
|
||||||
const verb = 'requires'
|
|
||||||
const description = `${this.hostApp.title} ${verb} an install of ${this.dep.title} satisfying ${this.dep.versionSpec}.`
|
|
||||||
|
|
||||||
const whyDependency = this.dep.description
|
|
||||||
|
|
||||||
const installationRecommendation: Recommendation = {
|
|
||||||
iconURL: this.hostApp.iconURL,
|
|
||||||
appId: this.hostApp.id,
|
|
||||||
description,
|
|
||||||
title: this.hostApp.title,
|
|
||||||
versionSpec: this.dep.versionSpec,
|
|
||||||
whyDependency,
|
|
||||||
}
|
|
||||||
const navigationExtras: NavigationExtras = {
|
|
||||||
state: { installationRecommendation },
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`, navigationExtras)
|
|
||||||
}
|
|
||||||
|
|
||||||
async configure () {
|
|
||||||
if (this.dep.violation.name !== 'incompatible-config') return
|
|
||||||
const configViolationDesc = this.dep.violation.ruleViolations
|
|
||||||
|
|
||||||
const configViolationFormatted =
|
|
||||||
`<ul>${configViolationDesc.map(d => `<li>${d}</li>`).join('\n')}</ul>`
|
|
||||||
|
|
||||||
const configRecommendation: Recommendation = {
|
|
||||||
iconURL: this.hostApp.iconURL,
|
|
||||||
appId: this.hostApp.id,
|
|
||||||
description: configViolationFormatted,
|
|
||||||
title: this.hostApp.title,
|
|
||||||
}
|
|
||||||
const navigationExtras: NavigationExtras = {
|
|
||||||
state: { configRecommendation },
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.navCtrl.navigateForward(`/services/installed/${this.dep.id}/config`, navigationExtras)
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentAlertDescription() {
|
|
||||||
const description = `<p>${this.dep.description}<\p>`
|
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
backdropDismiss: true,
|
|
||||||
message: description,
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="{ loading: $loading$ | async, disabled: installing || ($loading$ | async) } as l" >
|
|
||||||
<ion-item
|
|
||||||
class="dependency"
|
|
||||||
style="--border-color: var(--ion-color-medium-shade)"
|
|
||||||
[lines]="presentAlertDescription ? 'inset' : 'full'"
|
|
||||||
>
|
|
||||||
<ion-avatar style="position: relative; height: 5vh; width: 5vh; margin: 0px;" slot="start">
|
|
||||||
<div *ngIf="!l.loading" class="badge" [style]="badgeStyle"></div>
|
|
||||||
<img [src]="dep.iconURL | iconParse" />
|
|
||||||
</ion-avatar>
|
|
||||||
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
|
|
||||||
<h4 style="font-family: 'Montserrat'">{{ dep.title }}
|
|
||||||
<span *ngIf="recommended" style="font-family: 'Open Sans'; font-size: small; color: var(--ion-color-medium)">(recommended)</span>
|
|
||||||
</h4>
|
|
||||||
<p style="font-size: small">{{ dep.versionSpec }}</p>
|
|
||||||
<p *ngIf="!l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="color">{{statusText}}</ion-text></p>
|
|
||||||
<p *ngIf="l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text color="medium">Refreshing</ion-text></p>
|
|
||||||
</ion-label>
|
|
||||||
|
|
||||||
<ion-button size="small" (click)="presentAlertDescription=!presentAlertDescription" [disabled]="l.loading" color="medium" fill="clear" style="margin: 14px; font-size: small">
|
|
||||||
<ion-icon *ngIf="!presentAlertDescription" name="chevron-down"></ion-icon>
|
|
||||||
<ion-icon *ngIf="presentAlertDescription" name="chevron-up"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
|
|
||||||
<ion-button size="small" (click)="toInstall()" *ngIf="!installing && !l.loading" color="primary" fill="outline" style="font-size: small">
|
|
||||||
{{actionText}}
|
|
||||||
</ion-button>
|
|
||||||
|
|
||||||
<div slot="end" *ngIf='installing || (l.loading)' style="margin: 0" >
|
|
||||||
<div *ngIf='installing && !(l.loading)' class="spinner">
|
|
||||||
<ion-spinner [color]="color" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf='(l.loading)' class="spinner">
|
|
||||||
<ion-spinner [color]="medium" style="height: 3vh; width: 3vh" name="lines"></ion-spinner>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item style="margin-bottom: 10px"*ngIf="presentAlertDescription" lines="none">
|
|
||||||
<div style="font-size: small; color: var(--ion-color-medium)" [innerHtml]="descriptionText"></div>
|
|
||||||
</ion-item>
|
|
||||||
<div style="height: 8px"></div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { MarketplaceDependencyItemComponent } from './marketplace-dependency-item.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { RouterModule } from '@angular/router'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module'
|
|
||||||
import { StatusComponentModule } from '../../status/status.component.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [MarketplaceDependencyItemComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild([]),
|
|
||||||
SharingModule,
|
|
||||||
InformationPopoverComponentModule,
|
|
||||||
StatusComponentModule,
|
|
||||||
],
|
|
||||||
exports: [MarketplaceDependencyItemComponent],
|
|
||||||
})
|
|
||||||
export class MarketplaceDependencyItemComponentModule { }
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
.spinner {
|
|
||||||
background: rgba(0,0,0,0);
|
|
||||||
border-radius: 100px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: absolute; width: 2.5vh;
|
|
||||||
height: 2.5vh;
|
|
||||||
border-radius: 50px;
|
|
||||||
left: -1vh;
|
|
||||||
top: -1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xSmallText {
|
|
||||||
font-size: x-small !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mediumText {
|
|
||||||
font-size: medium !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opacityUp {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dependency {
|
|
||||||
--padding-start: 20px;
|
|
||||||
--padding-end: 2px;
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
|
||||||
import { NavigationExtras } from '@angular/router'
|
|
||||||
import { NavController } from '@ionic/angular'
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs'
|
|
||||||
import { AppDependency, BaseApp, DependencyViolationSeverity, getViolationSeverity, isOptional, isMissing, isInstalling, isRecommended, isVersionMismatch } from 'src/app/models/app-types'
|
|
||||||
import { Recommendation } from '../../recommendation-button/recommendation-button.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'marketplace-dependency-item',
|
|
||||||
templateUrl: './marketplace-dependency-item.component.html',
|
|
||||||
styleUrls: ['./marketplace-dependency-item.component.scss'],
|
|
||||||
})
|
|
||||||
export class MarketplaceDependencyItemComponent implements OnInit {
|
|
||||||
@Input() dep: AppDependency
|
|
||||||
@Input() hostApp: BaseApp
|
|
||||||
@Input() $loading$: BehaviorSubject<boolean>
|
|
||||||
|
|
||||||
presentAlertDescription = false
|
|
||||||
|
|
||||||
isLoading$: Observable<boolean>
|
|
||||||
color: string
|
|
||||||
installing = false
|
|
||||||
recommended = false
|
|
||||||
badgeStyle: string
|
|
||||||
violationSeverity: DependencyViolationSeverity
|
|
||||||
statusText: string
|
|
||||||
actionText: 'View' | 'Get'
|
|
||||||
|
|
||||||
descriptionText: string
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private readonly navCtrl: NavController,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.violationSeverity = getViolationSeverity(this.dep)
|
|
||||||
if (isOptional(this.dep)) throw new Error('Do not display optional deps, satisfied or otherwise, on the AAL')
|
|
||||||
|
|
||||||
const { actionText, color, statusText, installing } = this.getValues()
|
|
||||||
|
|
||||||
this.color = color
|
|
||||||
this.statusText = statusText
|
|
||||||
this.installing = installing
|
|
||||||
this.recommended = isRecommended(this.dep)
|
|
||||||
this.actionText = actionText
|
|
||||||
this.badgeStyle = `background: radial-gradient(var(--ion-color-${this.color}) 40%, transparent)`
|
|
||||||
this.descriptionText = `<p>${this.dep.description}<\p>`
|
|
||||||
if (this.recommended) {
|
|
||||||
this.descriptionText = this.descriptionText + `<p>This service is not required: ${this.dep.optional}<\p>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isDanger (): boolean {
|
|
||||||
return [DependencyViolationSeverity.REQUIRED, DependencyViolationSeverity.RECOMMENDED].includes(this.violationSeverity)
|
|
||||||
}
|
|
||||||
|
|
||||||
getValues (): { color: string, statusText: string, installing: boolean, actionText: 'View' | 'Get' } {
|
|
||||||
if (isInstalling(this.dep)) return { color: 'primary', statusText: 'Installing', installing: true, actionText: undefined }
|
|
||||||
if (!this.isDanger()) return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View' }
|
|
||||||
if (isMissing(this.dep)) return { color: 'warning', statusText: 'Not Installed', installing: false, actionText: 'Get' }
|
|
||||||
if (isVersionMismatch(this.dep)) return { color: 'warning', statusText: 'Incompatible Version Installed', installing: false, actionText: 'Get' }
|
|
||||||
return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View' }
|
|
||||||
}
|
|
||||||
|
|
||||||
async toInstall () {
|
|
||||||
if (this.actionText === 'View') return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`)
|
|
||||||
|
|
||||||
const verb = this.violationSeverity === DependencyViolationSeverity.REQUIRED ? 'requires' : 'recommends'
|
|
||||||
const description = `${this.hostApp.title} ${verb} an install of ${this.dep.title} satisfying ${this.dep.versionSpec}.`
|
|
||||||
|
|
||||||
const whyDependency = this.dep.description
|
|
||||||
|
|
||||||
const installationRecommendation: Recommendation = {
|
|
||||||
iconURL: this.hostApp.iconURL,
|
|
||||||
appId: this.hostApp.id,
|
|
||||||
description,
|
|
||||||
title: this.hostApp.title,
|
|
||||||
versionSpec: this.dep.versionSpec,
|
|
||||||
whyDependency,
|
|
||||||
}
|
|
||||||
const navigationExtras: NavigationExtras = {
|
|
||||||
state: { installationRecommendation },
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`, navigationExtras)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<ion-item lines="none" *ngIf="$error$ | async as error" class="notifier-item" style="margin-top: 12px">
|
|
||||||
<ion-label class="ion-text-wrap" color="danger"><p>{{ error }}</p></ion-label>
|
|
||||||
<ion-button *ngIf="dismissable" size="small" slot="end" fill="outline" color="danger" (click)="clear()">
|
|
||||||
<ion-icon style="height: 12px; width: 12px;" name="close"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { ErrorMessageComponent } from './error-message.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { RouterModule } from '@angular/router'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
ErrorMessageComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild([]),
|
|
||||||
SharingModule,
|
|
||||||
],
|
|
||||||
exports: [ErrorMessageComponent],
|
|
||||||
})
|
|
||||||
export class ErrorMessageComponentModule { }
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.error-message {
|
|
||||||
--background: var(--ion-color-danger);
|
|
||||||
margin: 12px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legacy-error-message {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'error-message',
|
|
||||||
templateUrl: './error-message.component.html',
|
|
||||||
styleUrls: ['./error-message.component.scss'],
|
|
||||||
})
|
|
||||||
export class ErrorMessageComponent implements OnInit {
|
|
||||||
@Input() $error$: BehaviorSubject<string | undefined> = new BehaviorSubject(undefined)
|
|
||||||
@Input() dismissable = true
|
|
||||||
|
|
||||||
constructor () { }
|
|
||||||
ngOnInit () { }
|
|
||||||
|
|
||||||
clear () {
|
|
||||||
this.$error$.next(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div *ngIf="!($loading$ | async) && !params.skipCompletionDialogue" class="slide-content">
|
<div *ngIf="!(loading$ | ngrxPush) && !params.skipCompletionDialogue" class="slide-content">
|
||||||
<div style="margin-top: 25px;">
|
<div style="margin-top: 25px;">
|
||||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||||
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
|
<ion-label [color]="$color$ | ngrxPush" style="font-size: xx-large; font-weight: bold;">
|
||||||
{{successText}}
|
{{successText}}
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="$loading$ | async" class="center-spinner">
|
<div *ngIf="loading$ | ngrxPush" class="center-spinner">
|
||||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||||
<ion-label class="long-message">{{label}}</ion-label>
|
<ion-label class="long-message">{{label}}</ion-label>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,17 +28,18 @@ export class CompleteComponent implements OnInit, Loadable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$loading$ = new BehaviorSubject(false)
|
loading$ = new BehaviorSubject(false)
|
||||||
$color$ = new BehaviorSubject('medium')
|
color$ = new BehaviorSubject('medium')
|
||||||
$cancel$ = new Subject<void>()
|
cancel$ = new Subject<void>()
|
||||||
|
|
||||||
label: string
|
label: string
|
||||||
summary: string
|
summary: string
|
||||||
successText: string
|
successText: string
|
||||||
|
|
||||||
load () {
|
load () {
|
||||||
markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe(
|
markAsLoadingDuring$(this.loading$, from(this.params.executeAction())).pipe(takeUntil(this.cancel$)).subscribe(
|
||||||
{ error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
|
{
|
||||||
|
error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
|
||||||
complete: () => this.params.skipCompletionDialogue && this.transitions.final(),
|
complete: () => this.params.skipCompletionDialogue && this.transitions.final(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -50,37 +51,37 @@ export class CompleteComponent implements OnInit, Loadable {
|
|||||||
case 'install':
|
case 'install':
|
||||||
this.summary = `Installation of ${this.params.title} is now in progress. You will receive a notification when the installation has completed.`
|
this.summary = `Installation of ${this.params.title} is now in progress. You will receive a notification when the installation has completed.`
|
||||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||||
this.$color$.next('primary')
|
this.color$.next('primary')
|
||||||
this.successText = 'In Progress'
|
this.successText = 'In Progress'
|
||||||
break
|
break
|
||||||
case 'downgrade':
|
case 'downgrade':
|
||||||
this.summary = `Downgrade for ${this.params.title} is now in progress. You will receive a notification when the downgrade has completed.`
|
this.summary = `Downgrade for ${this.params.title} is now in progress. You will receive a notification when the downgrade has completed.`
|
||||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||||
this.$color$.next('primary')
|
this.color$.next('primary')
|
||||||
this.successText = 'In Progress'
|
this.successText = 'In Progress'
|
||||||
break
|
break
|
||||||
case 'update':
|
case 'update':
|
||||||
this.summary = `Update for ${this.params.title} is now in progress. You will receive a notification when the update has completed.`
|
this.summary = `Update for ${this.params.title} is now in progress. You will receive a notification when the update has completed.`
|
||||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||||
this.$color$.next('primary')
|
this.color$.next('primary')
|
||||||
this.successText = 'In Progress'
|
this.successText = 'In Progress'
|
||||||
break
|
break
|
||||||
case 'uninstall':
|
case 'uninstall':
|
||||||
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully uninstalled.`
|
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully uninstalled.`
|
||||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||||
this.$color$.next('success')
|
this.color$.next('success')
|
||||||
this.successText = 'Success'
|
this.successText = 'Success'
|
||||||
break
|
break
|
||||||
case 'stop':
|
case 'stop':
|
||||||
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully stopped.`
|
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully stopped.`
|
||||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||||
this.$color$.next('success')
|
this.color$.next('success')
|
||||||
this.successText = 'Success'
|
this.successText = 'Success'
|
||||||
break
|
break
|
||||||
case 'configure':
|
case 'configure':
|
||||||
this.summary = `New config for ${this.params.title} has been successfully saved.`
|
this.summary = `New config for ${this.params.title} has been successfully saved.`
|
||||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||||
this.$color$.next('success')
|
this.color$.next('success')
|
||||||
this.successText = 'Success'
|
this.successText = 'Success'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<div class="slide-content">
|
|
||||||
<div style="margin-top: 25px;">
|
|
||||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
|
||||||
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
|
|
||||||
{{label}}
|
|
||||||
</ion-label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="long-message">
|
|
||||||
{{longMessage}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin: 25px 0px;">
|
|
||||||
<ion-item
|
|
||||||
style="--ion-item-background: rgb(0,0,0,0); margin-top: 5px"
|
|
||||||
*ngFor="let dep of dependencyViolations"
|
|
||||||
>
|
|
||||||
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
|
|
||||||
<div class="badge" [style]="dep.badgeStyle"></div>
|
|
||||||
<img [src]="dep.iconURL | iconParse" />
|
|
||||||
</ion-avatar>
|
|
||||||
<ion-label>
|
|
||||||
<h5>{{dep.title}}</h5>
|
|
||||||
<ion-text color="medium" style="font-size: smaller">{{dep.versionSpec}}</ion-text>
|
|
||||||
</ion-label>
|
|
||||||
<ion-text [color]="dep.color" style="font-size: smaller; font-style: italic; margin-right: 5px;">{{dep.violation}}</ion-text>
|
|
||||||
<status *ngIf="dep.isInstalling" [appStatus]="'INSTALLING'" size="italics-small"></status>
|
|
||||||
</ion-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { DependenciesComponent } from './dependencies.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { RouterModule } from '@angular/router'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
import { StatusComponentModule } from '../../status/status.component.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
DependenciesComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild([]),
|
|
||||||
SharingModule,
|
|
||||||
StatusComponentModule,
|
|
||||||
],
|
|
||||||
exports: [DependenciesComponent],
|
|
||||||
})
|
|
||||||
export class DependenciesComponentModule { }
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
|
||||||
import { PopoverController } from '@ionic/angular'
|
|
||||||
import { BehaviorSubject, Subject } from 'rxjs'
|
|
||||||
import { AppStatus } from 'src/app/models/app-model'
|
|
||||||
import { AppDependency, DependencyViolationSeverity, getViolationSeverity } from 'src/app/models/app-types'
|
|
||||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
|
||||||
import { InformationPopoverComponent } from '../../information-popover/information-popover.component'
|
|
||||||
import { Loadable } from '../loadable'
|
|
||||||
import { WizardAction } from '../wizard-types'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'dependencies',
|
|
||||||
templateUrl: './dependencies.component.html',
|
|
||||||
styleUrls: ['../install-wizard.component.scss'],
|
|
||||||
})
|
|
||||||
export class DependenciesComponent implements OnInit, Loadable {
|
|
||||||
@Input() params: {
|
|
||||||
action: WizardAction,
|
|
||||||
title: string,
|
|
||||||
version: string,
|
|
||||||
serviceRequirements: AppDependency[]
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredServiceRequirements: AppDependency[]
|
|
||||||
|
|
||||||
$loading$ = new BehaviorSubject(false)
|
|
||||||
$cancel$ = new Subject<void>()
|
|
||||||
|
|
||||||
longMessage: string
|
|
||||||
dependencyViolations: {
|
|
||||||
iconURL: string
|
|
||||||
title: string,
|
|
||||||
versionSpec: string,
|
|
||||||
violation: string,
|
|
||||||
color: string,
|
|
||||||
badgeStyle: string
|
|
||||||
}[]
|
|
||||||
label: string
|
|
||||||
$color$ = new BehaviorSubject('medium')
|
|
||||||
|
|
||||||
constructor (private readonly popoverController: PopoverController) { }
|
|
||||||
|
|
||||||
load () {
|
|
||||||
this.$color$.next(this.$color$.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.filteredServiceRequirements = this.params.serviceRequirements.filter(dep => {
|
|
||||||
return [DependencyViolationSeverity.REQUIRED, DependencyViolationSeverity.RECOMMENDED].includes(getViolationSeverity(dep))
|
|
||||||
})
|
|
||||||
.filter(dep => ['incompatible-version', 'missing'].includes(dep.violation.name))
|
|
||||||
|
|
||||||
this.dependencyViolations = this.filteredServiceRequirements
|
|
||||||
.map(dep => ({
|
|
||||||
iconURL: dep.iconURL,
|
|
||||||
title: dep.title,
|
|
||||||
versionSpec: (dep.violation && dep.violation.name === 'incompatible-config' && 'reconfigure') || dep.versionSpec,
|
|
||||||
isInstalling: dep.violation && dep.violation.name === 'incompatible-status' && dep.violation.status === AppStatus.INSTALLING,
|
|
||||||
violation: renderViolation(dep),
|
|
||||||
color: 'medium',
|
|
||||||
badgeStyle: `background: radial-gradient(var(--ion-color-warning) 40%, transparent)`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
this.setSeverityAttributes()
|
|
||||||
}
|
|
||||||
|
|
||||||
setSeverityAttributes () {
|
|
||||||
switch (getWorstViolationSeverity(this.filteredServiceRequirements)){
|
|
||||||
case DependencyViolationSeverity.REQUIRED:
|
|
||||||
this.longMessage = `${this.params.title} requires the installation of other services. Don't worry, you'll be able to install these requirements later.`
|
|
||||||
this.label = 'Notice'
|
|
||||||
this.$color$.next('dark')
|
|
||||||
break
|
|
||||||
case DependencyViolationSeverity.RECOMMENDED:
|
|
||||||
this.longMessage = `${this.params.title} recommends the installation of other services. Don't worry, you'll be able to install these requirements later.`
|
|
||||||
this.label = 'Notice'
|
|
||||||
this.$color$.next('dark')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this.longMessage = `All installation requirements for ${this.params.title} version ${displayEmver(this.params.version)} are met.`
|
|
||||||
this.$color$.next('success')
|
|
||||||
this.label = `Ready`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentPopover (ev: any, information: string) {
|
|
||||||
const popover = await this.popoverController.create({
|
|
||||||
component: InformationPopoverComponent,
|
|
||||||
event: ev,
|
|
||||||
translucent: false,
|
|
||||||
showBackdrop: true,
|
|
||||||
backdropDismiss: true,
|
|
||||||
componentProps: {
|
|
||||||
information,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return popover.present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderViolation1 (dep: AppDependency): string {
|
|
||||||
const severity = getViolationSeverity(dep)
|
|
||||||
switch (severity){
|
|
||||||
case DependencyViolationSeverity.REQUIRED: return 'mandatory'
|
|
||||||
case DependencyViolationSeverity.RECOMMENDED: return 'recommended'
|
|
||||||
case DependencyViolationSeverity.OPTIONAL: return 'optional'
|
|
||||||
case DependencyViolationSeverity.NONE: return 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderViolation (dep: AppDependency): string {
|
|
||||||
const severity = renderViolation1(dep)
|
|
||||||
if (severity === 'none') return ''
|
|
||||||
|
|
||||||
switch (dep.violation.name){
|
|
||||||
case 'missing': return `${severity}`
|
|
||||||
case 'incompatible-version': return `${severity}`
|
|
||||||
case 'incompatible-config': return ``
|
|
||||||
case 'incompatible-status': return ''
|
|
||||||
default: return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorstViolationSeverity (rs: AppDependency[]) : DependencyViolationSeverity {
|
|
||||||
if (!rs) return DependencyViolationSeverity.NONE
|
|
||||||
return rs.map(getViolationSeverity).sort( (a, b) => b - a )[0] || DependencyViolationSeverity.NONE
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<div *ngIf="!($loading$ | async)" class="slide-content">
|
<div *ngIf="!(loading$ | ngrxPush)" class="slide-content">
|
||||||
<div style="margin-top: 25px;">
|
<div style="margin-top: 25px;">
|
||||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||||
<ion-label color="warning" style="font-size: xx-large; font-weight: bold;"
|
<ion-label color="warning" style="font-size: xx-large; font-weight: bold;"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
*ngFor="let dep of dependentBreakages"
|
*ngFor="let dep of dependentBreakages"
|
||||||
>
|
>
|
||||||
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
|
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
|
||||||
<img [src]="dep.iconURL | iconParse" />
|
<img [src]="dep.iconURL" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h5>{{dep.title}}</h5>
|
<h5>{{dep.title}}</h5>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="$loading$ | async" class="center-spinner">
|
<div *ngIf="loading$ | ngrxPush" class="center-spinner">
|
||||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||||
<ion-label class="long-message">Checking for installed services which depend on {{params.title}}...</ion-label>
|
<ion-label class="long-message">Checking for installed services which depend on {{params.title}}...</ion-label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input, OnInit } from '@angular/core'
|
||||||
import { BehaviorSubject, from, Subject } from 'rxjs'
|
import { BehaviorSubject, from, Subject } from 'rxjs'
|
||||||
import { takeUntil, tap } from 'rxjs/operators'
|
import { takeUntil, tap } from 'rxjs/operators'
|
||||||
import { DependentBreakage } from 'src/app/models/app-types'
|
import { Breakages } from 'src/app/services/api/api-types'
|
||||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
import { capitalizeFirstLetter, isEmptyObject } from 'src/app/util/misc.util'
|
||||||
import { Loadable } from '../loadable'
|
import { Loadable } from '../loadable'
|
||||||
import { WizardAction } from '../wizard-types'
|
import { WizardAction } from '../wizard-types'
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export class DependentsComponent implements OnInit, Loadable {
|
|||||||
title: string,
|
title: string,
|
||||||
action: WizardAction, //Are you sure you want to *uninstall*...,
|
action: WizardAction, //Are you sure you want to *uninstall*...,
|
||||||
verb: string, // *Uninstalling* will cause problems...
|
verb: string, // *Uninstalling* will cause problems...
|
||||||
fetchBreakages: () => Promise<DependentBreakage[]>,
|
fetchBreakages: () => Promise<Breakages>,
|
||||||
skipConfirmationDialogue?: boolean
|
skipConfirmationDialogue?: boolean
|
||||||
}
|
}
|
||||||
@Input() transitions: {
|
@Input() transitions: {
|
||||||
@@ -27,34 +27,33 @@ export class DependentsComponent implements OnInit, Loadable {
|
|||||||
error: (e: Error) => void
|
error: (e: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependentBreakages: Breakages
|
||||||
dependentBreakages: DependentBreakage[]
|
|
||||||
hasDependentViolation: boolean
|
hasDependentViolation: boolean
|
||||||
longMessage: string | null = null
|
longMessage: string | null = null
|
||||||
$color$ = new BehaviorSubject('medium') // this will display disabled while loading
|
color$ = new BehaviorSubject('medium') // this will display disabled while loading
|
||||||
$loading$ = new BehaviorSubject(false)
|
loading$ = new BehaviorSubject(false)
|
||||||
$cancel$ = new Subject<void>()
|
cancel$ = new Subject<void>()
|
||||||
|
|
||||||
constructor () { }
|
constructor () { }
|
||||||
ngOnInit () { }
|
ngOnInit () { }
|
||||||
|
|
||||||
load () {
|
load () {
|
||||||
this.$color$.next('medium')
|
this.color$.next('medium')
|
||||||
markAsLoadingDuring$(this.$loading$, from(this.params.fetchBreakages())).pipe(
|
markAsLoadingDuring$(this.loading$, from(this.params.fetchBreakages())).pipe(
|
||||||
takeUntil(this.$cancel$),
|
takeUntil(this.cancel$),
|
||||||
tap(breakages => this.dependentBreakages = breakages || []),
|
tap(breakages => this.dependentBreakages = breakages),
|
||||||
).subscribe(
|
).subscribe(
|
||||||
{
|
{
|
||||||
complete: () => {
|
complete: () => {
|
||||||
this.hasDependentViolation = this.dependentBreakages && this.dependentBreakages.length > 0
|
this.hasDependentViolation = this.dependentBreakages && !isEmptyObject(this.dependentBreakages)
|
||||||
if (this.hasDependentViolation) {
|
if (this.hasDependentViolation) {
|
||||||
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
|
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
|
||||||
this.$color$.next('warning')
|
this.color$.next('warning')
|
||||||
} else if (this.params.skipConfirmationDialogue) {
|
} else if (this.params.skipConfirmationDialogue) {
|
||||||
this.transitions.next()
|
this.transitions.next()
|
||||||
} else {
|
} else {
|
||||||
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
|
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
|
||||||
this.$color$.next('success')
|
this.color$.next('success')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (e: Error) => this.transitions.error(new Error(`Fetching dependent service information failed: ${e.message || e}`)),
|
error: (e: Error) => this.transitions.error(new Error(`Fetching dependent service information failed: ${e.message || e}`)),
|
||||||
|
|||||||
@@ -8,18 +8,18 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
|
<ion-slides *ngIf="!(error$ | ngrxPush)" id="slide-show" style="--bullet-background: white" pager="false">
|
||||||
<ion-slide *ngFor="let def of params.slideDefinitions">
|
<ion-slide *ngFor="let def of params.slideDefinitions">
|
||||||
<!-- We can pass [transitions]="transitions" into the component if logic within the component needs to trigger a transition (not just bottom bar) -->
|
<!-- We can pass [transitions]="transitions" into the component if logic within the component needs to trigger a transition (not just bottom bar) -->
|
||||||
<dependencies #components *ngIf="def.slide.selector === 'dependencies'" [params]="def.slide.params"></dependencies>
|
<dependencies #components *ngIf="def.slide.selector === 'dependencies'" [params]="def.slide.params"></dependencies>
|
||||||
<notes #components *ngIf="def.slide.selector === 'notes'" [params]="def.slide.params"></notes>
|
<notes #components *ngIf="def.slide.selector === 'notes'" [params]="def.slide.params" style="width: 100%;"></notes>
|
||||||
<dependents #components *ngIf="def.slide.selector === 'dependents'" [params]="def.slide.params" [transitions]="transitions"></dependents>
|
<dependents #components *ngIf="def.slide.selector === 'dependents'" [params]="def.slide.params" [transitions]="transitions"></dependents>
|
||||||
<complete #components *ngIf="def.slide.selector === 'complete'" [params]="def.slide.params" [transitions]="transitions"></complete>
|
<complete #components *ngIf="def.slide.selector === 'complete'" [params]="def.slide.params" [transitions]="transitions"></complete>
|
||||||
</ion-slide>
|
</ion-slide>
|
||||||
</ion-slides>
|
</ion-slides>
|
||||||
|
|
||||||
|
|
||||||
<div *ngIf="$error$ | async as error" class="slide-content">
|
<div *ngIf="error$ | ngrxPush as error" class="slide-content">
|
||||||
<div style="margin-top: 25px;">
|
<div style="margin-top: 25px;">
|
||||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||||
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
|
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<ion-footer>
|
<ion-footer>
|
||||||
<ion-toolbar style="padding: 8px;">
|
<ion-toolbar style="padding: 8px;">
|
||||||
<ng-container *ngIf="!($initializing$ | async) && !($error$ | async) && { loading: currentSlide.$loading$ | async, bar: currentBottomBar} as v">
|
<ng-container *ngIf="!(initializing$ | ngrxPush) && !(error$ | ngrxPush) && { loading: currentSlideloading$ | ngrxPush, bar: currentBottomBar} as v">
|
||||||
|
|
||||||
<!-- cancel button if loading/not loading -->
|
<!-- cancel button if loading/not loading -->
|
||||||
<ion-button slot="start" *ngIf="v.loading && v.bar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
|
<ion-button slot="start" *ngIf="v.loading && v.bar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="$error$ | async">
|
<ng-container *ngIf="error$ | ngrxPush">
|
||||||
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { InstallWizardComponent } from './install-wizard.component'
|
|||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
|
|
||||||
import { DependentsComponentModule } from './dependents/dependents.component.module'
|
import { DependentsComponentModule } from './dependents/dependents.component.module'
|
||||||
import { CompleteComponentModule } from './complete/complete.component.module'
|
import { CompleteComponentModule } from './complete/complete.component.module'
|
||||||
import { NotesComponentModule } from './notes/notes.component.module'
|
import { NotesComponentModule } from './notes/notes.component.module'
|
||||||
@@ -18,7 +17,6 @@ import { NotesComponentModule } from './notes/notes.component.module'
|
|||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild([]),
|
RouterModule.forChild([]),
|
||||||
SharingModule,
|
SharingModule,
|
||||||
DependenciesComponentModule,
|
|
||||||
DependentsComponentModule,
|
DependentsComponentModule,
|
||||||
CompleteComponentModule,
|
CompleteComponentModule,
|
||||||
NotesComponentModule,
|
NotesComponentModule,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
import { Component, Input, NgZone, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||||
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
|
||||||
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
|
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
|
||||||
import { CompleteComponent } from './complete/complete.component'
|
import { CompleteComponent } from './complete/complete.component'
|
||||||
import { DependenciesComponent } from './dependencies/dependencies.component'
|
|
||||||
import { DependentsComponent } from './dependents/dependents.component'
|
import { DependentsComponent } from './dependents/dependents.component'
|
||||||
import { NotesComponent } from './notes/notes.component'
|
import { NotesComponent } from './notes/notes.component'
|
||||||
import { Loadable } from './loadable'
|
import { Loadable } from './loadable'
|
||||||
@@ -15,7 +13,7 @@ import { WizardAction } from './wizard-types'
|
|||||||
templateUrl: './install-wizard.component.html',
|
templateUrl: './install-wizard.component.html',
|
||||||
styleUrls: ['./install-wizard.component.scss'],
|
styleUrls: ['./install-wizard.component.scss'],
|
||||||
})
|
})
|
||||||
export class InstallWizardComponent extends Cleanup implements OnInit {
|
export class InstallWizardComponent {
|
||||||
@Input() params: {
|
@Input() params: {
|
||||||
// defines each slide along with bottom bar
|
// defines each slide along with bottom bar
|
||||||
slideDefinitions: SlideDefinition[]
|
slideDefinitions: SlideDefinition[]
|
||||||
@@ -40,11 +38,13 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
|||||||
return this.params.slideDefinitions[this.slideIndex].bottomBar
|
return this.params.slideDefinitions[this.slideIndex].bottomBar
|
||||||
}
|
}
|
||||||
|
|
||||||
$initializing$ = new BehaviorSubject(true)
|
initializing$ = new BehaviorSubject(true)
|
||||||
$error$ = new BehaviorSubject(undefined)
|
error$ = new BehaviorSubject(undefined)
|
||||||
|
|
||||||
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
|
constructor (
|
||||||
ngOnInit () { }
|
private readonly modalController: ModalController,
|
||||||
|
private readonly zone: NgZone,
|
||||||
|
) { }
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
this.currentSlide.load()
|
this.currentSlide.load()
|
||||||
@@ -53,15 +53,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidEnter () {
|
ionViewDidEnter () {
|
||||||
this.$initializing$.next(false)
|
this.initializing$.next(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// process bottom bar buttons
|
// process bottom bar buttons
|
||||||
private transition = (info: { next: any } | { error: Error } | { cancelled: true } | { final: true }) => {
|
private transition = (info: { next: any } | { error: Error } | { cancelled: true } | { final: true }) => {
|
||||||
const i = info as { next?: any, error?: Error, cancelled?: true, final?: true }
|
const i = info as { next?: any, error?: Error, cancelled?: true, final?: true }
|
||||||
if (i.cancelled) this.currentSlide.$cancel$.next()
|
if (i.cancelled) this.currentSlide.cancel$.next()
|
||||||
if (i.final || i.cancelled) return this.modalController.dismiss(i)
|
if (i.final || i.cancelled) return this.modalController.dismiss(i)
|
||||||
if (i.error) return this.$error$.next(capitalizeFirstLetter(i.error.message))
|
if (i.error) return this.error$.next(capitalizeFirstLetter(i.error.message))
|
||||||
|
|
||||||
this.moveToNextSlide(i.next)
|
this.moveToNextSlide(i.next)
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,6 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
|||||||
|
|
||||||
export interface SlideDefinition {
|
export interface SlideDefinition {
|
||||||
slide:
|
slide:
|
||||||
{ selector: 'dependencies', params: DependenciesComponent['params'] } |
|
|
||||||
{ selector: 'dependents', params: DependentsComponent['params'] } |
|
{ selector: 'dependents', params: DependentsComponent['params'] } |
|
||||||
{ selector: 'complete', params: CompleteComponent['params'] } |
|
{ selector: 'complete', params: CompleteComponent['params'] } |
|
||||||
{ selector: 'notes', params: NotesComponent['params'] }
|
{ selector: 'notes', params: NotesComponent['params'] }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { BehaviorSubject, Subject } from 'rxjs'
|
|||||||
export interface Loadable {
|
export interface Loadable {
|
||||||
load: (prevResult?: any) => void
|
load: (prevResult?: any) => void
|
||||||
result?: any // fill this variable on slide 1 to get passed into the load on slide 2. If this variable is falsey, it will skip the next slide.
|
result?: any // fill this variable on slide 1 to get passed into the load on slide 2. If this variable is falsey, it will skip the next slide.
|
||||||
$loading$: BehaviorSubject<boolean> // will be true during load function
|
loading$: BehaviorSubject<boolean> // will be true during load function
|
||||||
$cancel$: Subject<void> // will cancel load function
|
cancel$: Subject<void> // will cancel load function
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export class NotesComponent implements OnInit, Loadable {
|
|||||||
titleColor: string
|
titleColor: string
|
||||||
}
|
}
|
||||||
|
|
||||||
$loading$ = new BehaviorSubject(false)
|
loading$ = new BehaviorSubject(false)
|
||||||
$cancel$ = new Subject<void>()
|
cancel$ = new Subject<void>()
|
||||||
|
|
||||||
load () { }
|
load () { }
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
import { Breakages } from 'src/app/services/api/api-types'
|
||||||
import { exists } from 'src/app/util/misc.util'
|
import { exists } from 'src/app/util/misc.util'
|
||||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
|
||||||
import { ApiService } from '../../services/api/api.service'
|
import { ApiService } from '../../services/api/api.service'
|
||||||
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
||||||
import { WizardAction } from './wizard-types'
|
import { WizardAction } from './wizard-types'
|
||||||
@@ -11,19 +10,16 @@ import { WizardAction } from './wizard-types'
|
|||||||
export class WizardBaker {
|
export class WizardBaker {
|
||||||
constructor (
|
constructor (
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly updateService: OsUpdateService,
|
|
||||||
private readonly appModel: AppModel,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
install (values: {
|
install (values: {
|
||||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
id: string, title: string, version: string, installAlert?: string
|
||||||
}): InstallWizardComponent['params'] {
|
}): InstallWizardComponent['params'] {
|
||||||
const { id, title, version, serviceRequirements, installAlert } = values
|
const { id, title, version, installAlert } = values
|
||||||
|
|
||||||
validate(id, exists, 'missing id')
|
validate(id, exists, 'missing id')
|
||||||
validate(title, exists, 'missing title')
|
validate(title, exists, 'missing title')
|
||||||
validate(version, exists, 'missing version')
|
validate(version, exists, 'missing version')
|
||||||
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
|
|
||||||
|
|
||||||
const action = 'install'
|
const action = 'install'
|
||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
@@ -32,22 +28,16 @@ export class WizardBaker {
|
|||||||
installAlert ? {
|
installAlert ? {
|
||||||
slide: {
|
slide: {
|
||||||
selector: 'notes',
|
selector: 'notes',
|
||||||
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
|
params: {
|
||||||
|
notes: installAlert,
|
||||||
|
title: 'Warning',
|
||||||
|
titleColor: 'warning',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next',
|
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next',
|
||||||
},
|
},
|
||||||
} : undefined,
|
} : undefined,
|
||||||
{
|
|
||||||
slide: {
|
|
||||||
selector: 'dependencies',
|
|
||||||
params: { action, title, version, serviceRequirements },
|
|
||||||
},
|
|
||||||
bottomBar: {
|
|
||||||
cancel: { afterLoading: { text: 'Cancel' } },
|
|
||||||
next: 'Install',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
slide: {
|
slide: {
|
||||||
selector: 'complete',
|
selector: 'complete',
|
||||||
@@ -55,7 +45,7 @@ export class WizardBaker {
|
|||||||
action,
|
action,
|
||||||
verb: 'beginning installation for',
|
verb: 'beginning installation for',
|
||||||
title,
|
title,
|
||||||
executeAction: () => this.apiService.installApp(id, version).then(app => { this.appModel.add({ ...app, status: AppStatus.INSTALLING })}),
|
executeAction: () => this.apiService.installPackage({ id, version }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -68,14 +58,13 @@ export class WizardBaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update (values: {
|
update (values: {
|
||||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
id: string, title: string, version: string, installAlert?: string
|
||||||
}): InstallWizardComponent['params'] {
|
}): InstallWizardComponent['params'] {
|
||||||
const { id, title, version, serviceRequirements, installAlert } = values
|
const { id, title, version, installAlert } = values
|
||||||
|
|
||||||
validate(id, exists, 'missing id')
|
validate(id, exists, 'missing id')
|
||||||
validate(title, exists, 'missing title')
|
validate(title, exists, 'missing title')
|
||||||
validate(version, exists, 'missing version')
|
validate(version, exists, 'missing version')
|
||||||
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
|
|
||||||
|
|
||||||
const action = 'update'
|
const action = 'update'
|
||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
@@ -84,26 +73,26 @@ export class WizardBaker {
|
|||||||
installAlert ? {
|
installAlert ? {
|
||||||
slide: {
|
slide: {
|
||||||
selector: 'notes',
|
selector: 'notes',
|
||||||
params: { notes: installAlert, title: 'Warning', titleColor: 'warning'},
|
params: {
|
||||||
|
notes: installAlert,
|
||||||
|
title: 'Warning',
|
||||||
|
titleColor: 'warning',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
cancel: { afterLoading: { text: 'Cancel' } },
|
cancel: { afterLoading: { text: 'Cancel' } },
|
||||||
next: 'Next',
|
next: 'Next',
|
||||||
},
|
},
|
||||||
} : undefined,
|
} : undefined,
|
||||||
{ slide: {
|
{
|
||||||
selector: 'dependencies',
|
slide: {
|
||||||
params: { action, title, version, serviceRequirements },
|
|
||||||
},
|
|
||||||
bottomBar: {
|
|
||||||
cancel: { afterLoading: { text: 'Cancel' } },
|
|
||||||
next: 'Update',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ slide: {
|
|
||||||
selector: 'dependents',
|
selector: 'dependents',
|
||||||
params: {
|
params: {
|
||||||
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
skipConfirmationDialogue: true,
|
||||||
|
action,
|
||||||
|
verb: 'updating',
|
||||||
|
title,
|
||||||
|
fetchBreakages: () => this.apiService.dryUpdatePackage({ id, version }).then( ({ breakages }) => breakages ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -111,12 +100,14 @@ export class WizardBaker {
|
|||||||
next: 'Update Anyways',
|
next: 'Update Anyways',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'complete',
|
selector: 'complete',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
action,
|
||||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
verb: 'beginning update for',
|
||||||
}),
|
title,
|
||||||
|
executeAction: () => this.apiService.installPackage({ id, version }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -138,18 +129,27 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ slide : {
|
{
|
||||||
|
slide : {
|
||||||
selector: 'notes',
|
selector: 'notes',
|
||||||
params: { notes: releaseNotes, title: 'Release Notes', titleColor: 'dark' },
|
params: {
|
||||||
|
notes: releaseNotes,
|
||||||
|
title: 'Release Notes',
|
||||||
|
titleColor: 'dark',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
|
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'complete',
|
selector: 'complete',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
|
action,
|
||||||
|
verb: 'beginning update for',
|
||||||
|
title,
|
||||||
|
executeAction: () => this.apiService.updateServer({ }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -162,14 +162,13 @@ export class WizardBaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downgrade (values: {
|
downgrade (values: {
|
||||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
id: string, title: string, version: string, installAlert?: string
|
||||||
}): InstallWizardComponent['params'] {
|
}): InstallWizardComponent['params'] {
|
||||||
const { id, title, version, serviceRequirements, installAlert } = values
|
const { id, title, version, installAlert } = values
|
||||||
|
|
||||||
validate(id, exists, 'missing id')
|
validate(id, exists, 'missing id')
|
||||||
validate(title, exists, 'missing title')
|
validate(title, exists, 'missing title')
|
||||||
validate(version, exists, 'missing version')
|
validate(version, exists, 'missing version')
|
||||||
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
|
|
||||||
|
|
||||||
const action = 'downgrade'
|
const action = 'downgrade'
|
||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
@@ -178,23 +177,22 @@ export class WizardBaker {
|
|||||||
installAlert ? {
|
installAlert ? {
|
||||||
slide: {
|
slide: {
|
||||||
selector: 'notes',
|
selector: 'notes',
|
||||||
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
|
params: {
|
||||||
|
notes: installAlert,
|
||||||
|
title: 'Warning',
|
||||||
|
titleColor: 'warning',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next' },
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next' },
|
||||||
} : undefined,
|
} : undefined,
|
||||||
{ slide: {
|
|
||||||
selector: 'dependencies',
|
|
||||||
params: { action, title, version, serviceRequirements },
|
|
||||||
},
|
|
||||||
bottomBar: {
|
|
||||||
cancel: { afterLoading: { text: 'Cancel' } },
|
|
||||||
next: 'Downgrade',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ slide: {
|
{ slide: {
|
||||||
selector: 'dependents',
|
selector: 'dependents',
|
||||||
params: {
|
params: {
|
||||||
skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
skipConfirmationDialogue: true,
|
||||||
|
action,
|
||||||
|
verb: 'downgrading',
|
||||||
|
title,
|
||||||
|
fetchBreakages: () => this.apiService.dryUpdatePackage({ id, version }).then( ({ breakages }) => breakages ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -204,9 +202,10 @@ export class WizardBaker {
|
|||||||
{ slide: {
|
{ slide: {
|
||||||
selector: 'complete',
|
selector: 'complete',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
action,
|
||||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
verb: 'beginning downgrade for',
|
||||||
}),
|
title,
|
||||||
|
executeAction: () => this.apiService.installPackage({ id, version }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
@@ -231,7 +230,8 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'notes',
|
selector: 'notes',
|
||||||
params: {
|
params: {
|
||||||
notes: uninstallAlert || defaultUninstallationWarning(title),
|
notes: uninstallAlert || defaultUninstallationWarning(title),
|
||||||
@@ -241,18 +241,26 @@ export class WizardBaker {
|
|||||||
},
|
},
|
||||||
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Continue' },
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Continue' },
|
||||||
},
|
},
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'dependents',
|
selector: 'dependents',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
|
action,
|
||||||
|
verb: 'uninstalling',
|
||||||
|
title,
|
||||||
|
fetchBreakages: () => this.apiService.dryRemovePackage({ id }).then( ({ breakages }) => breakages ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' },
|
bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' },
|
||||||
},
|
},
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'complete',
|
selector: 'complete',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
|
action,
|
||||||
|
verb: 'uninstalling',
|
||||||
|
title,
|
||||||
|
executeAction: () => this.apiService.removePackage({ id }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },
|
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },
|
||||||
@@ -262,7 +270,7 @@ export class WizardBaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop (values: {
|
stop (values: {
|
||||||
breakages: DependentBreakage[], id: string, title: string, version: string
|
breakages: Breakages, id: string, title: string, version: string
|
||||||
}): InstallWizardComponent['params'] {
|
}): InstallWizardComponent['params'] {
|
||||||
const { breakages, title, version } = values
|
const { breakages, title, version } = values
|
||||||
|
|
||||||
@@ -274,10 +282,14 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'dependents',
|
selector: 'dependents',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
|
action,
|
||||||
|
verb: 'stopping',
|
||||||
|
title,
|
||||||
|
fetchBreakages: () => Promise.resolve(breakages),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Stop Anyways' },
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Stop Anyways' },
|
||||||
@@ -286,19 +298,20 @@ export class WizardBaker {
|
|||||||
return { toolbar, slideDefinitions }
|
return { toolbar, slideDefinitions }
|
||||||
}
|
}
|
||||||
|
|
||||||
configure (values: {
|
configure (values: { breakages: Breakages, pkg: InstalledPackageDataEntry }): InstallWizardComponent['params'] {
|
||||||
breakages: DependentBreakage[], app: AppInstalledPreview
|
const { breakages, pkg } = values
|
||||||
}): InstallWizardComponent['params'] {
|
const { title, version } = pkg.manifest
|
||||||
const { breakages, app } = values
|
|
||||||
const { title, versionInstalled: version } = app
|
|
||||||
const action = 'configure'
|
const action = 'configure'
|
||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ slide: {
|
{
|
||||||
|
slide: {
|
||||||
selector: 'dependents',
|
selector: 'dependents',
|
||||||
params: {
|
params: {
|
||||||
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
|
action,
|
||||||
|
verb: 'saving config for',
|
||||||
|
title, fetchBreakages: () => Promise.resolve(breakages),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Save Config Anyways' },
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Save Config Anyways' },
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
import { Annotation, Annotations } from '../../app-config/config-utilities'
|
import { Annotation, Annotations } from '../../pkg-config/config-utilities'
|
||||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||||
import { ModalPresentable } from 'src/app/app-config/modal-presentable'
|
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||||
import { ValueSpecOf, ValueSpec } from 'src/app/app-config/config-types'
|
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
|
||||||
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { PwaBackService } from 'src/app/services/pwa-back.service'
|
import { NavController } from '@ionic/angular'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pwa-back-button',
|
selector: 'pwa-back-button',
|
||||||
templateUrl: './pwa-back.component.html',
|
templateUrl: './pwa-back.component.html',
|
||||||
@@ -7,10 +8,10 @@ import { PwaBackService } from 'src/app/services/pwa-back.service'
|
|||||||
})
|
})
|
||||||
export class PwaBackComponent {
|
export class PwaBackComponent {
|
||||||
constructor (
|
constructor (
|
||||||
private readonly pwaBack: PwaBackService,
|
private readonly nav: NavController,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
navigateBack () {
|
navigateBack () {
|
||||||
return this.pwaBack.back()
|
return this.nav.back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
border-color: #FFEB3B;
|
border-color: #FFEB3B;
|
||||||
border-width: medium;
|
border-width: medium;
|
||||||
box-shadow: 0 0 10px white;" size="small" (click)="presentPopover($event)">
|
box-shadow: 0 0 10px white;" size="small" (click)="presentPopover($event)">
|
||||||
<img [src]="rec.iconURL | iconParse" />
|
<img [src]="rec.iconURL" />
|
||||||
</ion-fab-button>
|
</ion-fab-button>
|
||||||
</ion-fab>
|
</ion-fab>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { PopoverController } from '@ionic/angular'
|
import { PopoverController } from '@ionic/angular'
|
||||||
import { filter, take } from 'rxjs/operators'
|
import { filter, take } from 'rxjs/operators'
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
|
||||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||||
import { InformationPopoverComponent } from '../information-popover/information-popover.component'
|
import { InformationPopoverComponent } from '../information-popover/information-popover.component'
|
||||||
|
|
||||||
@@ -11,12 +10,13 @@ import { InformationPopoverComponent } from '../information-popover/information-
|
|||||||
templateUrl: './recommendation-button.component.html',
|
templateUrl: './recommendation-button.component.html',
|
||||||
styleUrls: ['./recommendation-button.component.scss'],
|
styleUrls: ['./recommendation-button.component.scss'],
|
||||||
})
|
})
|
||||||
export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
export class RecommendationButtonComponent {
|
||||||
@Input() rec: Recommendation
|
@Input() rec: Recommendation
|
||||||
@Input() raise?: { id: string }
|
@Input() raise?: { id: string }
|
||||||
constructor (private readonly router: Router, private readonly popoverController: PopoverController) {
|
constructor (
|
||||||
super()
|
private readonly router: Router,
|
||||||
}
|
private readonly popoverController: PopoverController,
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
if (!this.raise) return
|
if (!this.raise) return
|
||||||
@@ -41,7 +41,7 @@ export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
information: `
|
information: `
|
||||||
<div style="font-size: medium; font-style: italic; margin: 5px 0px;">
|
<div style="font-size: medium; font-style: italic; margin: 5px 0px;">
|
||||||
${capitalizeFirstLetter(this.rec.title)} Installation Recommendations
|
${capitalizeFirstLetter(this.rec.dependentTitle)} Installation Recommendations
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${this.rec.description}
|
${this.rec.description}
|
||||||
@@ -57,10 +57,9 @@ export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Recommendation = {
|
export type Recommendation = {
|
||||||
title: string
|
dependentId: string
|
||||||
appId: string
|
dependentTitle: string
|
||||||
iconURL: string,
|
dependentIcon: string,
|
||||||
description: string,
|
description: string
|
||||||
versionSpec?: string
|
version?: string
|
||||||
whyDependency?: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<p *ngIf="size === 'small'" style="margin: 0 0 4px 0;">
|
<p *ngIf="size === 'small'" style="margin: 0 0 4px 0;">
|
||||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
<ion-text [color]="color">{{ display }}</ion-text>
|
||||||
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 *ngIf="size === 'italics-small'" style="margin: 0 0 4px 0;">
|
<p *ngIf="size === 'italics-small'" style="margin: 0 0 4px 0; font-style: italic;">
|
||||||
<ion-text [style]="style" style="font-size: small; font-style: italic; text-transform: lowercase;" [color]="color">{{ display }}</ion-text>
|
<ion-text [color]="color">{{ display }}</ion-text>
|
||||||
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
||||||
</h3>
|
</p>
|
||||||
|
|
||||||
<h3 *ngIf="size === 'medium'">
|
<h3 *ngIf="size === 'medium'">
|
||||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
<ion-text [color]="color">{{ display }}</ion-text>
|
||||||
<ion-spinner *ngIf="showDots" class="dots dots-medium" name="dots" [color]="color"></ion-spinner>
|
<ion-spinner *ngIf="showDots" class="dots dots-medium" name="dots" [color]="color"></ion-spinner>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<h1 *ngIf="size === 'large'">
|
<h1 *ngIf="size === 'large'">
|
||||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
<ion-text [color]="color">{{ display }}</ion-text>
|
||||||
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h1 style="font-size: 18px; font-weight: 500" *ngIf="size === 'bold-large'">
|
<h1 *ngIf="size === 'bold-large'" style="font-size: 18px; font-weight: 500">
|
||||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
<ion-text [color]="color">{{ display }}</ion-text>
|
||||||
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -15,18 +15,3 @@
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dots {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dots-small {
|
|
||||||
width: 12px !important;
|
|
||||||
height: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dots-medium {
|
|
||||||
width: 16px !important;
|
|
||||||
height: 16px !important;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { AppStatus } from 'src/app/models/app-model'
|
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||||
import { ServerStatus } from 'src/app/models/server-model'
|
import { ConnectionState } from 'src/app/services/connection.service'
|
||||||
import { ServerStatusRendering, AppStatusRendering } from '../../util/status-rendering'
|
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'status',
|
selector: 'status',
|
||||||
@@ -9,48 +9,18 @@ import { ServerStatusRendering, AppStatusRendering } from '../../util/status-ren
|
|||||||
styleUrls: ['./status.component.scss'],
|
styleUrls: ['./status.component.scss'],
|
||||||
})
|
})
|
||||||
export class StatusComponent {
|
export class StatusComponent {
|
||||||
@Input() appStatus?: AppStatus
|
@Input() pkg: PackageDataEntry
|
||||||
@Input() serverStatus?: ServerStatus
|
@Input() connection: ConnectionState
|
||||||
@Input() size: 'small' | 'medium' | 'large' | 'italics-small' | 'bold-large' = 'large'
|
@Input() size: 'small' | 'medium' | 'large' | 'italics-small' | 'bold-large' = 'large'
|
||||||
@Input() text: string = ''
|
display = ''
|
||||||
color: string
|
color = ''
|
||||||
display: string
|
showDots = false
|
||||||
showDots: boolean
|
|
||||||
style = ''
|
|
||||||
|
|
||||||
ngOnChanges () {
|
ngOnChanges () {
|
||||||
if (this.serverStatus) {
|
const { display, color, showDots } = renderPkgStatus(this.pkg, this.connection)
|
||||||
this.handleServerStatus()
|
|
||||||
} else if (this.appStatus) {
|
|
||||||
this.handleAppStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleServerStatus () {
|
|
||||||
let res = ServerStatusRendering[this.serverStatus]
|
|
||||||
if (!res) {
|
|
||||||
console.warn(`Received invalid server status from the server: `, this.serverStatus)
|
|
||||||
res = ServerStatusRendering[ServerStatus.UNKNOWN]
|
|
||||||
}
|
|
||||||
|
|
||||||
const { display, color, showDots } = res
|
|
||||||
this.display = display
|
this.display = display
|
||||||
this.color = color
|
this.color = color
|
||||||
this.showDots = showDots
|
this.showDots = showDots
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAppStatus () {
|
|
||||||
let res = AppStatusRendering[this.appStatus]
|
|
||||||
if (!res) {
|
|
||||||
console.warn(`Received invalid app status from the server: `, this.appStatus)
|
|
||||||
res = AppStatusRendering[AppStatus.UNKNOWN]
|
|
||||||
}
|
|
||||||
|
|
||||||
const { display, color, showDots, style } = res
|
|
||||||
this.display = display + this.text
|
|
||||||
this.color = color
|
|
||||||
this.showDots = showDots
|
|
||||||
this.style = style
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<ion-item button lines="none" *ngIf="updateAvailable$ | async as res" (click)="confirmUpdate(res)">
|
|
||||||
<ion-label>
|
|
||||||
New EmbassyOS Version {{res.versionLatest | displayEmver}} Available!
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { UpdateOsBannerComponent } from './update-os-banner.component'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
UpdateOsBannerComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
SharingModule,
|
|
||||||
],
|
|
||||||
exports: [UpdateOsBannerComponent],
|
|
||||||
})
|
|
||||||
export class UpdateOsBannerComponentModule { }
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
ion-item {
|
|
||||||
|
|
||||||
--background: linear-gradient(90deg, var(--ion-color-light), var(--ion-color-primary));
|
|
||||||
--min-height: 0px;
|
|
||||||
ion-label {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
font-size: small;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Component } from '@angular/core'
|
|
||||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
|
||||||
import { Observable } from 'rxjs'
|
|
||||||
import { ModalController } from '@ionic/angular'
|
|
||||||
import { WizardBaker } from '../install-wizard/prebaked-wizards'
|
|
||||||
import { wizardModal } from '../install-wizard/install-wizard.component'
|
|
||||||
import { ReqRes } from 'src/app/services/api/api.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'update-os-banner',
|
|
||||||
templateUrl: './update-os-banner.component.html',
|
|
||||||
styleUrls: ['./update-os-banner.component.scss'],
|
|
||||||
})
|
|
||||||
export class UpdateOsBannerComponent {
|
|
||||||
updateAvailable$: Observable<undefined | ReqRes.GetVersionLatestRes>
|
|
||||||
constructor (
|
|
||||||
private readonly osUpdateService: OsUpdateService,
|
|
||||||
private readonly modalCtrl: ModalController,
|
|
||||||
private readonly wizardBaker: WizardBaker,
|
|
||||||
) {
|
|
||||||
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () { }
|
|
||||||
|
|
||||||
async confirmUpdate (res: ReqRes.GetVersionLatestRes) {
|
|
||||||
await wizardModal(
|
|
||||||
this.modalCtrl,
|
|
||||||
this.wizardBaker.updateOS({
|
|
||||||
version: res.versionLatest,
|
|
||||||
releaseNotes: res.releaseNotes,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,41 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { CanActivate, Router, CanActivateChild } from '@angular/router'
|
import { CanActivate, Router, CanActivateChild } from '@angular/router'
|
||||||
|
import { tap } from 'rxjs/operators'
|
||||||
import { AuthState, AuthService } from '../services/auth.service'
|
import { AuthState, AuthService } from '../services/auth.service'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthGuard implements CanActivate, CanActivateChild {
|
export class AuthGuard implements CanActivate, CanActivateChild {
|
||||||
|
authState: AuthState
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly router: Router,
|
private readonly router: Router,
|
||||||
) { }
|
) {
|
||||||
|
this.authService.watch$()
|
||||||
|
.pipe(
|
||||||
|
tap(auth => this.authState = auth),
|
||||||
|
).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
canActivate (): boolean {
|
canActivate (): boolean {
|
||||||
return this.runCheck()
|
return this.runAuthCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivateChild (): boolean {
|
canActivateChild (): boolean {
|
||||||
return this.runCheck()
|
return this.runAuthCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
private runCheck (): boolean {
|
private runAuthCheck (): boolean {
|
||||||
const state = this.authService.peek()
|
switch (this.authState){
|
||||||
|
case AuthState.VERIFIED:
|
||||||
switch (state){
|
return true
|
||||||
case AuthState.VERIFIED: return true
|
case AuthState.UNVERIFIED:
|
||||||
case AuthState.UNVERIFIED: return this.toAuthenticate()
|
// @TODO could initializing cause a loop?
|
||||||
case AuthState.INITIALIZING: return this.toAuthenticate()
|
case AuthState.INITIALIZING:
|
||||||
}
|
this.router.navigate(['/auth'], { replaceUrl: true })
|
||||||
}
|
|
||||||
|
|
||||||
private toAuthenticate () {
|
|
||||||
this.router.navigate(['/authenticate'], { replaceUrl: true })
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Injectable, Directive } from '@angular/core'
|
|
||||||
import { CanDeactivate } from '@angular/router'
|
|
||||||
import { HostListener } from '@angular/core'
|
|
||||||
|
|
||||||
@Directive()
|
|
||||||
export abstract class PageCanDeactivate {
|
|
||||||
abstract canDeactivate (): boolean
|
|
||||||
|
|
||||||
@HostListener('window:beforeunload', ['$event'])
|
|
||||||
unloadNotification (e: any) {
|
|
||||||
console.log(e)
|
|
||||||
if (!this.canDeactivate()) {
|
|
||||||
e.returnValue = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class CanDeactivateGuard implements CanDeactivate<PageCanDeactivate> {
|
|
||||||
|
|
||||||
canDeactivate (page: PageCanDeactivate): boolean {
|
|
||||||
return page.canDeactivate() || confirm('You have unsaved changes. Are you sure you want to leave the page?')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
39
ui/src/app/guards/maintenance.guard.ts
Normal file
39
ui/src/app/guards/maintenance.guard.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { CanActivate, Router, CanActivateChild } from '@angular/router'
|
||||||
|
import { tap } from 'rxjs/operators'
|
||||||
|
import { ServerStatus } from '../models/patch-db/data-model'
|
||||||
|
import { PatchDbModel } from '../models/patch-db/patch-db-model'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class MaintenanceGuard implements CanActivate, CanActivateChild {
|
||||||
|
serverStatus: ServerStatus
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private readonly router: Router,
|
||||||
|
private readonly patch: PatchDbModel,
|
||||||
|
) {
|
||||||
|
this.patch.watch$('server-info', 'status')
|
||||||
|
.pipe(
|
||||||
|
tap(status => this.serverStatus = status),
|
||||||
|
).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate (): boolean {
|
||||||
|
return this.runServerStatusCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivateChild (): boolean {
|
||||||
|
return this.runServerStatusCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
private runServerStatusCheck (): boolean {
|
||||||
|
if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(this.serverStatus)) {
|
||||||
|
this.router.navigate(['/maintenance'], { replaceUrl: true })
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,34 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { CanActivate, Router } from '@angular/router'
|
import { CanActivate, Router } from '@angular/router'
|
||||||
|
import { tap } from 'rxjs/operators'
|
||||||
import { AuthService, AuthState } from '../services/auth.service'
|
import { AuthService, AuthState } from '../services/auth.service'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UnauthGuard implements CanActivate {
|
export class UnauthGuard implements CanActivate {
|
||||||
|
authState: AuthState
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly router: Router,
|
private readonly router: Router,
|
||||||
) { }
|
) {
|
||||||
|
this.authService.watch$()
|
||||||
|
.pipe(
|
||||||
|
tap(auth => this.authState = auth),
|
||||||
|
).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
canActivate (): boolean {
|
canActivate (): boolean {
|
||||||
const state = this.authService.peek()
|
|
||||||
switch (state){
|
switch (this.authState){
|
||||||
case AuthState.VERIFIED: {
|
case AuthState.VERIFIED: {
|
||||||
this.router.navigateByUrl('')
|
this.router.navigateByUrl('')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case AuthState.UNVERIFIED: return true
|
case AuthState.UNVERIFIED:
|
||||||
case AuthState.INITIALIZING: return true
|
case AuthState.INITIALIZING:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
ui/src/app/guards/unmaintenance.guard.ts
Normal file
31
ui/src/app/guards/unmaintenance.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { CanActivate, Router } from '@angular/router'
|
||||||
|
import { tap } from 'rxjs/operators'
|
||||||
|
import { ServerStatus } from '../models/patch-db/data-model'
|
||||||
|
import { PatchDbModel } from '../models/patch-db/patch-db-model'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class UnmaintenanceGuard implements CanActivate {
|
||||||
|
serverStatus: ServerStatus
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private readonly router: Router,
|
||||||
|
private readonly patch: PatchDbModel,
|
||||||
|
) {
|
||||||
|
this.patch.watch$('server-info', 'status')
|
||||||
|
.pipe(
|
||||||
|
tap(status => this.serverStatus = status),
|
||||||
|
).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate (): boolean {
|
||||||
|
if (![ServerStatus.Updating, ServerStatus.BackingUp].includes(this.serverStatus)) {
|
||||||
|
this.router.navigate([''], { replaceUrl: true })
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { AppBackupPage } from './app-backup.page'
|
|
||||||
import { AppBackupConfirmationComponentModule } from 'src/app/components/app-backup-confirmation/app-backup-confirmation.component.module'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [AppBackupPage],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
AppBackupConfirmationComponentModule,
|
|
||||||
],
|
|
||||||
entryComponents: [AppBackupPage],
|
|
||||||
exports: [AppBackupPage],
|
|
||||||
})
|
|
||||||
export class AppBackupPageModule { }
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-button (click)="dismiss()">
|
|
||||||
<ion-icon name="close"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>{{ type === 'create' ? 'Create Backup' : 'Restore Backup' }}</ion-title>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button (click)="presentAlertHelp()" color="primary">
|
|
||||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
|
||||||
|
|
||||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
|
||||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
|
||||||
</ion-refresher>
|
|
||||||
|
|
||||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
|
||||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-spinner *ngIf="loading" class="center" name="lines" color="warning"></ion-spinner>
|
|
||||||
|
|
||||||
<ng-container *ngIf="!loading">
|
|
||||||
<ion-item *ngIf="type === 'restore' && (app.restoreAlert || defaultRestoreAlert) as restore" class="notifier-item" style="box-shadow: 0 0 5px 1px var(--ion-color-danger); margin-bottom: 40px">
|
|
||||||
<ion-label class="ion-text-wrap">
|
|
||||||
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
|
||||||
<ion-icon style="margin-right: 5px;" slot="start" color="danger" slot="start" name="warning-outline"></ion-icon>
|
|
||||||
<ion-text color="danger" style="font-size: medium; font-weight: bold">Warning</ion-text>
|
|
||||||
</h2>
|
|
||||||
<p style="font-size: small">{{restore}}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item *ngIf="allPartitionsMounted">
|
|
||||||
<ion-text *ngIf="type === 'create'" class="ion-text-wrap" color="warning">No partitions available. To begin a backup, insert a storage device into your Embassy.</ion-text>
|
|
||||||
<ion-text *ngIf="type === 'restore'" class="ion-text-wrap" color="warning">No partitions available. Insert the storage device containing the backup you wish to restore.</ion-text>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item-group *ngFor="let d of disks">
|
|
||||||
<ion-item-divider>{{ d.logicalname }} ({{ d.size }})</ion-item-divider>
|
|
||||||
<ion-item-group>
|
|
||||||
<ion-item button [disabled]="p.isMounted" *ngFor="let p of d.partitions" (click)="presentAlert(d, p)">
|
|
||||||
<ion-icon slot="start" name="save-outline"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ p.label || p.logicalname }}</h2>
|
|
||||||
<p>{{ p.size || 'unknown size' }}</p>
|
|
||||||
<p *ngIf="!p.isMounted"><ion-text color="success">Available</ion-text></p>
|
|
||||||
<p *ngIf="p.isMounted"><ion-text color="danger">Unvailable</ion-text></p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-item-group>
|
|
||||||
</ion-item-group>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</ion-content>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.toast-close-button {
|
|
||||||
color: var(--ion-color-primary) !important;
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
|
||||||
import { ModalController, AlertController, LoadingController, ToastController } from '@ionic/angular'
|
|
||||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
|
||||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
|
||||||
import { DiskInfo, DiskPartition } from 'src/app/models/server-model'
|
|
||||||
import { pauseFor } from 'src/app/util/misc.util'
|
|
||||||
import { concatMap } from 'rxjs/operators'
|
|
||||||
import { AppBackupConfirmationComponent } from 'src/app/components/app-backup-confirmation/app-backup-confirmation.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-backup',
|
|
||||||
templateUrl: './app-backup.page.html',
|
|
||||||
styleUrls: ['./app-backup.page.scss'],
|
|
||||||
})
|
|
||||||
export class AppBackupPage {
|
|
||||||
@Input() app: AppInstalledFull
|
|
||||||
@Input() type: 'create' | 'restore'
|
|
||||||
disks: DiskInfo[]
|
|
||||||
loading = true
|
|
||||||
error: string
|
|
||||||
allPartitionsMounted: boolean
|
|
||||||
defaultRestoreAlert: string
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private readonly modalCtrl: ModalController,
|
|
||||||
private readonly alertCtrl: AlertController,
|
|
||||||
private readonly loadingCtrl: LoadingController,
|
|
||||||
private readonly apiService: ApiService,
|
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit () {
|
|
||||||
this.defaultRestoreAlert = `Restoring ${this.app.title} will overwrite its current data.`
|
|
||||||
return this.getExternalDisks().then(() => this.loading = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExternalDisks (): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.disks = await this.apiService.getExternalDisks()
|
|
||||||
this.allPartitionsMounted = this.disks.every(d => d.partitions.every(p => p.isMounted))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
this.error = e.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async doRefresh (event: any) {
|
|
||||||
await Promise.all([
|
|
||||||
this.getExternalDisks(),
|
|
||||||
pauseFor(600),
|
|
||||||
])
|
|
||||||
event.target.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
async dismiss () {
|
|
||||||
await this.modalCtrl.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentAlertHelp (): Promise<void> {
|
|
||||||
let alert: HTMLIonAlertElement
|
|
||||||
if (this.type === 'create') {
|
|
||||||
alert = await this.alertCtrl.create({
|
|
||||||
backdropDismiss: false,
|
|
||||||
header: `Backups`,
|
|
||||||
message: `Select a location to back up ${this.app.title}.<br /><br />Internal drives and drives currently backing up other services will not be available.<br /><br />Depending on the amount of data in ${this.app.title}, your first backup may take a while. Since backups are diff-based, the speed of future backups to the same disk will likely be much faster.`,
|
|
||||||
buttons: ['Dismiss'],
|
|
||||||
})
|
|
||||||
} else if (this.type === 'restore') {
|
|
||||||
alert = await this.alertCtrl.create({
|
|
||||||
backdropDismiss: false,
|
|
||||||
header: `Backups`,
|
|
||||||
message: `Select a location containing the backup you wish to restore for ${this.app.title}.<br /><br />Restoring ${this.app.title} will re-sync your service with your previous backup. The speed of the restore process depends on the backup size.`,
|
|
||||||
buttons: ['Dismiss'],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentAlert (disk: DiskInfo, partition: DiskPartition): Promise<void> {
|
|
||||||
if (this.type === 'create') {
|
|
||||||
this.presentAlertCreateEncrypted(disk, partition)
|
|
||||||
} else {
|
|
||||||
this.presentAlertWarn(partition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async presentAlertCreateEncrypted (disk: DiskInfo, partition: DiskPartition): Promise<void> {
|
|
||||||
const m = await this.modalCtrl.create({
|
|
||||||
componentProps: {
|
|
||||||
app: this.app,
|
|
||||||
partition,
|
|
||||||
},
|
|
||||||
cssClass: 'alertlike-modal',
|
|
||||||
component: AppBackupConfirmationComponent,
|
|
||||||
backdropDismiss: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
m.onWillDismiss().then(res => {
|
|
||||||
const data = res.data
|
|
||||||
if (data.cancel) return
|
|
||||||
// TODO: EJECT-DISKS we hard code the 'eject' last argument to be false, until ejection is an option in the UI. When it is, add it to the data object above ^
|
|
||||||
return this.create(disk, partition, data.password, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
return await m.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async presentAlertWarn (partition: DiskPartition): Promise<void> {
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
backdropDismiss: false,
|
|
||||||
header: `Warning`,
|
|
||||||
message: `Restoring ${this.app.title} from "${partition.label || partition.logicalname}" will overwrite its current data.<br /><br />Are you sure you want to continue?`,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Cancel',
|
|
||||||
role: 'cancel',
|
|
||||||
}, {
|
|
||||||
text: 'Continue',
|
|
||||||
handler: () => {
|
|
||||||
this.presentAlertRestore(partition)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async presentAlertRestore (partition: DiskPartition): Promise<void> {
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
backdropDismiss: false,
|
|
||||||
header: `Decrypt Backup`,
|
|
||||||
message: `Enter your master password`,
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: 'password',
|
|
||||||
type: 'password',
|
|
||||||
placeholder: 'Password',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Cancel',
|
|
||||||
role: 'cancel',
|
|
||||||
}, {
|
|
||||||
text: 'Restore',
|
|
||||||
handler: (data) => {
|
|
||||||
this.restore(partition, data.password)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async restore (partition: DiskPartition, password?: string): Promise<void> {
|
|
||||||
this.error = ''
|
|
||||||
|
|
||||||
const loader = await this.loadingCtrl.create({
|
|
||||||
spinner: 'lines',
|
|
||||||
cssClass: 'loader-ontop-of-all',
|
|
||||||
})
|
|
||||||
await loader.present()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.apiService.restoreAppBackup(this.app.id, partition.logicalname, password)
|
|
||||||
this.appModel.update({ id: this.app.id, status: AppStatus.RESTORING_BACKUP })
|
|
||||||
await this.dismiss()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
this.error = e.message
|
|
||||||
} finally {
|
|
||||||
await loader.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async create (disk: DiskInfo, partition: DiskPartition, password: string, eject: boolean): Promise<void> {
|
|
||||||
this.error = ''
|
|
||||||
|
|
||||||
const loader = await this.loadingCtrl.create({
|
|
||||||
spinner: 'lines',
|
|
||||||
cssClass: 'loader-ontop-of-all',
|
|
||||||
})
|
|
||||||
await loader.present()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.apiService.createAppBackup(this.app.id, partition.logicalname, password)
|
|
||||||
this.appModel.update({ id: this.app.id, status: AppStatus.CREATING_BACKUP })
|
|
||||||
if (eject) {
|
|
||||||
this.appModel.watchForBackup(this.app.id).pipe(concatMap(
|
|
||||||
() => this.apiService.ejectExternalDisk(disk.logicalname),
|
|
||||||
)).subscribe({
|
|
||||||
next: () => this.toastEjection(disk, true),
|
|
||||||
error: () => this.toastEjection(disk, false),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await this.dismiss()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
this.error = e.message
|
|
||||||
} finally {
|
|
||||||
await loader.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async toastEjection (disk: DiskInfo, success: boolean) {
|
|
||||||
const { header, message, cssClass } = success ? {
|
|
||||||
header: 'Success',
|
|
||||||
message: `Drive ${disk.logicalname} ejected successfully`,
|
|
||||||
cssClass: 'notification-toast',
|
|
||||||
} : {
|
|
||||||
header: 'Error',
|
|
||||||
message: `Drive ${disk.logicalname} did not eject successfully`,
|
|
||||||
cssClass: 'alert-error-message',
|
|
||||||
}
|
|
||||||
const t = await this.toastCtrl.create({
|
|
||||||
header,
|
|
||||||
message,
|
|
||||||
cssClass,
|
|
||||||
duration: 2000,
|
|
||||||
})
|
|
||||||
await t.present()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Type } from '@angular/core'
|
import { Type } from '@angular/core'
|
||||||
import { ValueType } from 'src/app/app-config/config-types'
|
import { ValueType } from 'src/app/pkg-config/config-types'
|
||||||
|
|
||||||
export type AppConfigComponentMapping = { [k in ValueType]: Type<any> }
|
export type AppConfigComponentMapping = { [k in ValueType]: Type<any> }
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { AlertController } from '@ionic/angular'
|
import { AlertController } from '@ionic/angular'
|
||||||
import { Annotations, Range } from '../../app-config/config-utilities'
|
import { Annotations, Range } from '../../pkg-config/config-utilities'
|
||||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||||
import { ValueSpecList, isValueSpecListOf } from 'src/app/app-config/config-types'
|
import { ValueSpecList, isValueSpecListOf } from 'src/app/pkg-config/config-types'
|
||||||
import { ModalPresentable } from 'src/app/app-config/modal-presentable'
|
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-config-list',
|
selector: 'app-config-list',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ModalController, AlertController } from '@ionic/angular'
|
import { ModalController, AlertController } from '@ionic/angular'
|
||||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||||
import { ValueSpecObject } from 'src/app/app-config/config-types'
|
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-config-object',
|
selector: 'app-config-object',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Component, Input, ViewChild } from '@angular/core'
|
import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||||
import { ValueSpecUnion } from 'src/app/app-config/config-types'
|
import { ValueSpecUnion } from 'src/app/pkg-config/config-types'
|
||||||
import { ObjectConfigComponent } from 'src/app/components/object-config/object-config.component'
|
import { ObjectConfigComponent } from 'src/app/components/object-config/object-config.component'
|
||||||
import { mapUnionSpec } from '../../app-config/config-utilities'
|
import { mapUnionSpec } from '../../pkg-config/config-utilities'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-config-union',
|
selector: 'app-config-union',
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<p *ngIf="rangeDescription">
|
<p *ngIf="rangeDescription">
|
||||||
<ion-text color="medium">{{ rangeDescription }}</ion-text>
|
<ion-text color="medium">{{ rangeDescription }}</ion-text>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="spec.default">
|
<p *ngIf="spec.default !== undefined">
|
||||||
<ion-text color="medium">
|
<ion-text color="medium">
|
||||||
<p>Default: {{ defaultDescription }} <ion-icon style="padding-left: 8px;" name="refresh-outline" color="primary" (click)="refreshDefault()"></ion-icon></p>
|
<p>Default: {{ defaultDescription }} <ion-icon style="padding-left: 8px;" name="refresh-outline" color="primary" (click)="refreshDefault()"></ion-icon></p>
|
||||||
<p *ngIf="spec.type === 'number' && spec.units">Units: {{ spec.units }}</p>
|
<p *ngIf="spec.type === 'number' && spec.units">Units: {{ spec.units }}</p>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/app-config/config-utilities'
|
import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/pkg-config/config-utilities'
|
||||||
import { AlertController, ToastController } from '@ionic/angular'
|
import { AlertController, ToastController } from '@ionic/angular'
|
||||||
import { LoaderService } from 'src/app/services/loader.service'
|
import { LoaderService } from 'src/app/services/loader.service'
|
||||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||||
import { ValueSpecOf } from 'src/app/app-config/config-types'
|
import { ValueSpecOf } from 'src/app/pkg-config/config-types'
|
||||||
import { copyToClipboard } from 'src/app/util/web.util'
|
import { copyToClipboard } from 'src/app/util/web.util'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<ion-item lines="none" style="--background: var(--ion-background-color); --border-color: var(--ion-color-medium);">
|
<ion-item lines="none" style="--background: var(--ion-background-color); --border-color: var(--ion-color-medium);">
|
||||||
<ion-label style="font-size: small" position="floating">Master Password</ion-label>
|
<ion-label style="font-size: small" position="floating">Master Password</ion-label>
|
||||||
<ion-input style="border-style: solid; border-width: 0px 0px 1px 0px; border-color: var(--ion-color-dark);" [(ngModel)]="password" type="password" (ionChange)="$error$.next('')"></ion-input>
|
<ion-input style="border-style: solid; border-width: 0px 0px 1px 0px; border-color: var(--ion-color-dark);" [(ngModel)]="password" type="password" (ionChange)="error = ''"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="$error$ | async as e" lines="none" style="--background: var(--ion-background-color);">
|
<ion-item *ngIf="error" lines="none" style="--background: var(--ion-background-color);">
|
||||||
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{e}}</ion-label>
|
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{ error }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: flex-end; align-items: center;">
|
<div style="display: flex; justify-content: flex-end; align-items: center;">
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { AppBackupConfirmationComponent } from './app-backup-confirmation.component'
|
import { BackupConfirmationComponent } from './backup-confirmation.component'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppBackupConfirmationComponent,
|
BackupConfirmationComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -17,6 +17,6 @@ import { FormsModule } from '@angular/forms';
|
|||||||
SharingModule,
|
SharingModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
],
|
],
|
||||||
exports: [AppBackupConfirmationComponent],
|
exports: [BackupConfirmationComponent],
|
||||||
})
|
})
|
||||||
export class AppBackupConfirmationComponentModule { }
|
export class BackupConfirmationComponentModule { }
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { ModalController } from '@ionic/angular'
|
||||||
|
import { PartitionInfo } from 'src/app/services/api/api-types'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'backup-confirmation',
|
||||||
|
templateUrl: './backup-confirmation.component.html',
|
||||||
|
styleUrls: ['./backup-confirmation.component.scss'],
|
||||||
|
})
|
||||||
|
export class BackupConfirmationComponent {
|
||||||
|
@Input() name: string
|
||||||
|
unmasked = false
|
||||||
|
password: string
|
||||||
|
message: string
|
||||||
|
error = ''
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private readonly modalCtrl: ModalController,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.message = `Enter your master password to create an encrypted backup on "${this.name}".`
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMask () {
|
||||||
|
this.unmasked = !this.unmasked
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel () {
|
||||||
|
this.modalCtrl.dismiss({ cancel: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
submit () {
|
||||||
|
if (!this.password || this.password.length < 12) {
|
||||||
|
this.error = 'Password must be at least 12 characters in length.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { password } = this
|
||||||
|
this.modalCtrl.dismiss({ password })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { ServerModel } from 'src/app/models/server-model'
|
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ export class OSWelcomePage {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
async dismiss () {
|
async dismiss () {
|
||||||
this.apiService.acknowledgeOSWelcome(this.config.version).catch(console.error)
|
this.apiService.setDbValue({ pointer: '/welcome-ack', value: this.config.version }).catch(console.error)
|
||||||
|
|
||||||
// return false to skip subsequent alert modals (e.g. check for updates modals)
|
// return false to skip subsequent alert modals (e.g. check for updates modals)
|
||||||
// return true to show subsequent alert modals
|
// return true to show subsequent alert modals
|
||||||
|
|||||||
@@ -1,177 +1,153 @@
|
|||||||
import { MapSubject, Delta, Update } from '../util/map-subject.util'
|
// import { MapSubject, Delta, Update } from '../util/map-subject.util'
|
||||||
import { diff, partitionArray } from '../util/misc.util'
|
// import { diff, partitionArray } from '../util/misc.util'
|
||||||
import { PropertySubject, complete } from '../util/property-subject.util'
|
// import { Injectable } from '@angular/core'
|
||||||
import { Injectable } from '@angular/core'
|
// import { merge, Observable, of } from 'rxjs'
|
||||||
import { merge, Observable, of } from 'rxjs'
|
// import { filter, throttleTime, delay, pairwise, mapTo, take } from 'rxjs/operators'
|
||||||
import { filter, throttleTime, delay, pairwise, mapTo, take } from 'rxjs/operators'
|
// import { Storage } from '@ionic/storage'
|
||||||
import { Storage } from '@ionic/storage'
|
// import { StorageKeys } from './storage-keys'
|
||||||
import { StorageKeys } from './storage-keys'
|
// import { AppInstalledFull, AppInstalledPreview } from './app-types'
|
||||||
import { AppInstalledFull, AppInstalledPreview } from './app-types'
|
|
||||||
|
|
||||||
@Injectable({
|
// @Injectable({
|
||||||
providedIn: 'root',
|
// providedIn: 'root',
|
||||||
})
|
// })
|
||||||
export class AppModel extends MapSubject<AppInstalledFull> {
|
// export class AppModel extends MapSubject<AppInstalledFull> {
|
||||||
// hasLoaded tells us if we've successfully queried apps from api or storage, even if there are none.
|
// // hasLoaded tells us if we've successfully queried apps from api or storage, even if there are none.
|
||||||
hasLoaded = false
|
// hasLoaded = false
|
||||||
lastUpdatedAt: { [id: string]: Date } = { }
|
// lastUpdatedAt: { [id: string]: Date } = { }
|
||||||
constructor (private readonly storage: Storage) {
|
// constructor (private readonly storage: Storage) {
|
||||||
super()
|
// super()
|
||||||
// 500ms after first delta, will save to db. Subsequent deltas are ignored for those 500ms.
|
// // 500ms after first delta, will save to db. Subsequent deltas are ignored for those 500ms.
|
||||||
// Process continues as long as deltas fire.
|
// // Process continues as long as deltas fire.
|
||||||
this.watchDelta().pipe(throttleTime(200), delay(200)).subscribe(() => {
|
// this.watchDelta().pipe(throttleTime(200), delay(200)).subscribe(() => {
|
||||||
this.commitCache()
|
// this.commitCache()
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
update (newValues: Update<AppInstalledFull>, timestamp: Date = new Date()): void {
|
// update (newValues: Update<AppInstalledFull>, timestamp: Date = new Date()): void {
|
||||||
this.lastUpdatedAt[newValues.id] = this.lastUpdatedAt[newValues.id] || timestamp
|
// this.lastUpdatedAt[newValues.id] = this.lastUpdatedAt[newValues.id] || timestamp
|
||||||
if (this.lastUpdatedAt[newValues.id] > timestamp) {
|
// if (this.lastUpdatedAt[newValues.id] > timestamp) {
|
||||||
return
|
// return
|
||||||
} else {
|
// } else {
|
||||||
super.update(newValues)
|
// super.update(newValues)
|
||||||
this.lastUpdatedAt[newValues.id] = timestamp
|
// this.lastUpdatedAt[newValues.id] = timestamp
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// client fxns
|
// // client fxns
|
||||||
watchDelta (filterFor?: Delta<AppInstalledFull>['action']): Observable<Delta<AppInstalledFull>> {
|
// watchDelta (filterFor?: Delta<AppInstalledFull>['action']): Observable<Delta<AppInstalledFull>> {
|
||||||
return filterFor
|
// return filterFor
|
||||||
? this.$delta$.pipe(filter(d => d.action === filterFor))
|
// ? this.$delta$.pipe(filter(d => d.action === filterFor))
|
||||||
: this.$delta$.asObservable()
|
// : this.$delta$.asObservable()
|
||||||
}
|
// }
|
||||||
|
|
||||||
watch (appId: string) : PropertySubject<AppInstalledFull> {
|
// watch (appId: string) : PropertySubject<AppInstalledFull> {
|
||||||
const toReturn = super.watch(appId)
|
// const toReturn = super.watch(appId)
|
||||||
if (!toReturn) throw new Error(`Expected Service ${appId} but not found.`)
|
// if (!toReturn) throw new Error(`Expected Service ${appId} but not found.`)
|
||||||
return toReturn
|
// return toReturn
|
||||||
}
|
// }
|
||||||
|
|
||||||
// when an app is installing
|
// // when an app is installing
|
||||||
watchForInstallation (appId: string): Observable<string | undefined> {
|
// watchForInstallation (appId: string): Observable<string | undefined> {
|
||||||
const toWatch = super.watch(appId)
|
// const toWatch = super.watch(appId)
|
||||||
if (!toWatch) return of(undefined)
|
// if (!toWatch) return of(undefined)
|
||||||
|
|
||||||
return toWatch.status.pipe(
|
// return toWatch.status.pipe(
|
||||||
filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
|
// filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
|
||||||
pairwise(),
|
// pairwise(),
|
||||||
filter( ([old, _]) => old === AppStatus.INSTALLING ),
|
// filter( ([old, _]) => old === AppStatus.INSTALLING ),
|
||||||
take(1),
|
// take(1),
|
||||||
mapTo(appId),
|
// mapTo(appId),
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
// TODO: EJECT-DISKS: we can use this to watch for an app completing its backup process.
|
// // TODO: EJECT-DISKS: we can use this to watch for an app completing its backup process.
|
||||||
watchForBackup (appId: string): Observable<string | undefined> {
|
// watchForBackup (appId: string): Observable<string | undefined> {
|
||||||
const toWatch = super.watch(appId)
|
// const toWatch = super.watch(appId)
|
||||||
if (!toWatch) return of(undefined)
|
// if (!toWatch) return of(undefined)
|
||||||
|
|
||||||
return toWatch.status.pipe(
|
// return toWatch.status.pipe(
|
||||||
filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
|
// filter(s => s !== AppStatus.UNREACHABLE && s !== AppStatus.UNKNOWN),
|
||||||
pairwise(),
|
// pairwise(),
|
||||||
filter( ([old, _]) => old === AppStatus.CREATING_BACKUP),
|
// filter( ([old, _]) => old === AppStatus.CREATING_BACKUP),
|
||||||
take(1),
|
// take(1),
|
||||||
mapTo(appId),
|
// mapTo(appId),
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
watchForInstallations (appIds: { id: string }[]): Observable<string> {
|
// watchForInstallations (appIds: { id: string }[]): Observable<string> {
|
||||||
return merge(...appIds.map(({ id }) => this.watchForInstallation(id))).pipe(
|
// return merge(...appIds.map(({ id }) => this.watchForInstallation(id))).pipe(
|
||||||
filter(t => !!t),
|
// filter(t => !!t),
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
// cache mgmt
|
// // cache mgmt
|
||||||
clear (): void {
|
// clear (): void {
|
||||||
this.ids.forEach(id => {
|
// this.ids.forEach(id => {
|
||||||
complete(this.contents[id] || { } as PropertySubject<any>)
|
// complete(this.contents[id] || { } as PropertySubject<any>)
|
||||||
delete this.contents[id]
|
// delete this.contents[id]
|
||||||
})
|
// })
|
||||||
this.hasLoaded = false
|
// this.hasLoaded = false
|
||||||
this.contents = { }
|
// this.contents = { }
|
||||||
this.lastUpdatedAt = { }
|
// this.lastUpdatedAt = { }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private commitCache (): Promise<void> {
|
// private commitCache (): Promise<void> {
|
||||||
return this.storage.set(StorageKeys.APPS_CACHE_KEY, this.all || [])
|
// return this.storage.set(StorageKeys.APPS_CACHE_KEY, this.all || [])
|
||||||
}
|
// }
|
||||||
|
|
||||||
async restoreCache (): Promise<void> {
|
// async restoreCache (): Promise<void> {
|
||||||
const stored = await this.storage.get(StorageKeys.APPS_CACHE_KEY)
|
// const stored = await this.storage.get(StorageKeys.APPS_CACHE_KEY)
|
||||||
console.log(`restored app cache`, stored)
|
// console.log(`restored app cache`, stored)
|
||||||
if (stored) this.hasLoaded = true
|
// if (stored) this.hasLoaded = true
|
||||||
return (stored || []).map(c => this.add({ ...emptyAppInstalledFull(), ...c, status: AppStatus.UNKNOWN }))
|
// return (stored || []).map(c => this.add({ ...emptyAppInstalledFull(), ...c, status: AppStatus.UNKNOWN }))
|
||||||
}
|
// }
|
||||||
|
|
||||||
upsertAppFull (app: AppInstalledFull): void {
|
// upsertAppFull (app: AppInstalledFull): void {
|
||||||
this.update(app)
|
// this.update(app)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// synchronizers
|
// // synchronizers
|
||||||
upsertApps (apps: AppInstalledPreview[], timestamp: Date): void {
|
// upsertApps (apps: AppInstalledPreview[], timestamp: Date): void {
|
||||||
const [updates, creates] = partitionArray(apps, a => !!this.contents[a.id])
|
// const [updates, creates] = partitionArray(apps, a => !!this.contents[a.id])
|
||||||
updates.map(u => this.update(u, timestamp))
|
// updates.map(u => this.update(u, timestamp))
|
||||||
creates.map(c => this.add({ ...emptyAppInstalledFull(), ...c }))
|
// creates.map(c => this.add({ ...emptyAppInstalledFull(), ...c }))
|
||||||
}
|
// }
|
||||||
|
|
||||||
syncCache (upToDateApps : AppInstalledPreview[], timestamp: Date) {
|
// syncCache (upToDateApps : AppInstalledPreview[], timestamp: Date) {
|
||||||
this.hasLoaded = true
|
// this.hasLoaded = true
|
||||||
this.deleteNonexistentApps(upToDateApps)
|
// this.deleteNonexistentApps(upToDateApps)
|
||||||
this.upsertApps(upToDateApps, timestamp)
|
// this.upsertApps(upToDateApps, timestamp)
|
||||||
}
|
// }
|
||||||
|
|
||||||
private deleteNonexistentApps (apps: AppInstalledPreview[]): void {
|
// private deleteNonexistentApps (apps: AppInstalledPreview[]): void {
|
||||||
const currentAppIds = apps.map(a => a.id)
|
// const currentAppIds = apps.map(a => a.id)
|
||||||
const previousAppIds = Object.keys(this.contents)
|
// const previousAppIds = Object.keys(this.contents)
|
||||||
const appsToDelete = diff(previousAppIds, currentAppIds)
|
// const appsToDelete = diff(previousAppIds, currentAppIds)
|
||||||
appsToDelete.map(appId => this.delete(appId))
|
// appsToDelete.map(appId => this.delete(appId))
|
||||||
}
|
// }
|
||||||
|
|
||||||
// server state change
|
// // server state change
|
||||||
markAppsUnreachable (): void {
|
// markAppsUnreachable (): void {
|
||||||
this.updateAllApps({ status: AppStatus.UNREACHABLE })
|
// this.updateAllApps({ status: AppStatus.UNREACHABLE })
|
||||||
}
|
// }
|
||||||
|
|
||||||
markAppsUnknown (): void {
|
// markAppsUnknown (): void {
|
||||||
this.updateAllApps({ status: AppStatus.UNKNOWN })
|
// this.updateAllApps({ status: AppStatus.UNKNOWN })
|
||||||
}
|
// }
|
||||||
|
|
||||||
private updateAllApps (uniformUpdate: Partial<AppInstalledFull>) {
|
// private updateAllApps (uniformUpdate: Partial<AppInstalledFull>) {
|
||||||
this.ids.map(id => {
|
// this.ids.map(id => {
|
||||||
this.update(Object.assign(uniformUpdate, { id }))
|
// this.update(Object.assign(uniformUpdate, { id }))
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPreview> {
|
// function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPreview> {
|
||||||
return {
|
// return {
|
||||||
instructions: null,
|
// instructions: null,
|
||||||
lastBackup: null,
|
// lastBackup: null,
|
||||||
configuredRequirements: null,
|
// configuredRequirements: null,
|
||||||
hasFetchedFull: false,
|
// hasFetchedFull: false,
|
||||||
actions: [],
|
// actions: [],
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface Rules {
|
|
||||||
rule: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AppStatus {
|
|
||||||
// shared
|
|
||||||
UNKNOWN = 'UNKNOWN',
|
|
||||||
UNREACHABLE = 'UNREACHABLE',
|
|
||||||
INSTALLING = 'INSTALLING',
|
|
||||||
NEEDS_CONFIG = 'NEEDS_CONFIG',
|
|
||||||
RUNNING = 'RUNNING',
|
|
||||||
STOPPED = 'STOPPED',
|
|
||||||
CREATING_BACKUP = 'CREATING_BACKUP',
|
|
||||||
RESTORING_BACKUP = 'RESTORING_BACKUP',
|
|
||||||
CRASHED = 'CRASHED',
|
|
||||||
REMOVING = 'REMOVING',
|
|
||||||
DEAD = 'DEAD',
|
|
||||||
BROKEN_DEPENDENCIES = 'BROKEN_DEPENDENCIES',
|
|
||||||
STOPPING = 'STOPPING',
|
|
||||||
RESTARTING = 'RESTARTING',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
import { AppStatus } from './app-model'
|
|
||||||
|
|
||||||
/** APPS **/
|
|
||||||
|
|
||||||
export interface BaseApp {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
status: AppStatus | null
|
|
||||||
versionInstalled: string | null
|
|
||||||
iconURL: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// available
|
|
||||||
export interface AppAvailablePreview extends BaseApp {
|
|
||||||
versionLatest: string
|
|
||||||
descriptionShort: string
|
|
||||||
latestVersionTimestamp: Date //used for sorting AAL
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AppAvailableFull =
|
|
||||||
AppAvailablePreview & {
|
|
||||||
descriptionLong: string
|
|
||||||
versions: string[]
|
|
||||||
licenseName?: string // @TODO required for 0.3.0
|
|
||||||
licenseLink?: string // @TODO required for 0.3.0
|
|
||||||
} &
|
|
||||||
AppAvailableVersionSpecificInfo
|
|
||||||
|
|
||||||
|
|
||||||
export interface AppAvailableVersionSpecificInfo {
|
|
||||||
releaseNotes: string
|
|
||||||
serviceRequirements: AppDependency[]
|
|
||||||
versionViewing: string
|
|
||||||
installAlert?: string
|
|
||||||
}
|
|
||||||
// installed
|
|
||||||
|
|
||||||
export interface AppInstalledPreview extends BaseApp {
|
|
||||||
lanAddress?: string
|
|
||||||
torAddress: string
|
|
||||||
versionInstalled: string
|
|
||||||
lanUi: boolean
|
|
||||||
torUi: boolean
|
|
||||||
// FE state only
|
|
||||||
hasUI: boolean
|
|
||||||
launchable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppInstalledFull extends AppInstalledPreview {
|
|
||||||
licenseName?: string // @TODO required for 0.3.0
|
|
||||||
licenseLink?: string // @TODO required for 0.3.0
|
|
||||||
instructions: string | null
|
|
||||||
lastBackup: string | null
|
|
||||||
configuredRequirements: AppDependency[] | null // null if not yet configured
|
|
||||||
startAlert?: string
|
|
||||||
uninstallAlert?: string
|
|
||||||
restoreAlert?: string
|
|
||||||
actions: Actions
|
|
||||||
// FE state only
|
|
||||||
hasFetchedFull: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Actions = ServiceAction[]
|
|
||||||
export interface ServiceAction {
|
|
||||||
id: string,
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
warning?: string
|
|
||||||
allowedStatuses: AppStatus[]
|
|
||||||
}
|
|
||||||
export interface AppDependency extends InstalledAppDependency {
|
|
||||||
// explanation of why it *is* optional. null represents it is required.
|
|
||||||
optional: string | null
|
|
||||||
// whether it comes as defualt in the config. This will not be present on an installed app, as we only care
|
|
||||||
default: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InstalledAppDependency extends Omit<BaseApp, 'versionInstalled' | 'status'> {
|
|
||||||
// semver specification
|
|
||||||
versionSpec: string
|
|
||||||
|
|
||||||
// an optional description of how this dependency is utlitized by the host app
|
|
||||||
description: string | null
|
|
||||||
|
|
||||||
// how the requirement is failed, null means satisfied. If the dependency is optional, this should still be set as though it were required.
|
|
||||||
// This way I can say "it's optional, but also you would need to upgrade it to versionSpec" or "it's optional, but you don't even have it"
|
|
||||||
// Said another way, if violaion === null, then this thing as a requirement is straight up satisfied.
|
|
||||||
violation: DependencyViolation | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DependencyViolationSeverity {
|
|
||||||
NONE = 0,
|
|
||||||
OPTIONAL = 1,
|
|
||||||
RECOMMENDED = 2,
|
|
||||||
REQUIRED = 3,
|
|
||||||
}
|
|
||||||
export function getViolationSeverity (r: AppDependency): DependencyViolationSeverity {
|
|
||||||
if (!r.optional && r.violation) return DependencyViolationSeverity.REQUIRED
|
|
||||||
if (r.optional && r.default && r.violation) return DependencyViolationSeverity.RECOMMENDED
|
|
||||||
if (isOptional(r) && r.violation) return DependencyViolationSeverity.OPTIONAL
|
|
||||||
return DependencyViolationSeverity.NONE
|
|
||||||
}
|
|
||||||
|
|
||||||
// optional not recommended
|
|
||||||
export function isOptional (r: AppDependency): boolean {
|
|
||||||
return r.optional && !r.default
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRecommended (r: AppDependency): boolean {
|
|
||||||
return r.optional && r.default
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMissing (r: AppDependency) {
|
|
||||||
return r.violation && r.violation.name === 'missing'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMisconfigured (r: AppDependency) {
|
|
||||||
return r.violation && r.violation.name === 'incompatible-config'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNotRunning (r: AppDependency) {
|
|
||||||
return r.violation && r.violation.name === 'incompatible-status'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isVersionMismatch (r: AppDependency) {
|
|
||||||
return r.violation && r.violation.name === 'incompatible-version'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isInstalling (r: AppDependency) {
|
|
||||||
return r.violation && r.violation.name === 'incompatible-status' && r.violation.status === AppStatus.INSTALLING
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// both or none
|
|
||||||
export function getInstalledViolationSeverity (r: InstalledAppDependency): DependencyViolationSeverity {
|
|
||||||
if (r.violation) return DependencyViolationSeverity.REQUIRED
|
|
||||||
return DependencyViolationSeverity.NONE
|
|
||||||
}
|
|
||||||
// e.g. of I try to uninstall a thing, and some installed apps break, those apps will be returned as instances of this type.
|
|
||||||
export type DependentBreakage = Omit<BaseApp, 'versionInstalled' | 'status'>
|
|
||||||
|
|
||||||
export type DependencyViolation =
|
|
||||||
{ name: 'missing' } |
|
|
||||||
{ name: 'incompatible-version' } |
|
|
||||||
{ name: 'incompatible-config'; ruleViolations: string[]; } |
|
|
||||||
{ name: 'incompatible-status'; status: AppStatus; }
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { AppModel } from './app-model'
|
|
||||||
import { AppInstalledFull, AppInstalledPreview } from './app-types'
|
|
||||||
import { ApiService } from '../services/api/api.service'
|
|
||||||
import { PropertySubject, PropertySubjectId } from '../util/property-subject.util'
|
|
||||||
import { S9Server, ServerModel } from './server-model'
|
|
||||||
import { Observable, of, from } from 'rxjs'
|
|
||||||
import { map, concatMap } from 'rxjs/operators'
|
|
||||||
import { fromSync$ } from '../util/rxjs.util'
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class ModelPreload {
|
|
||||||
constructor (
|
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly api: ApiService,
|
|
||||||
private readonly serverModel: ServerModel,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
apps (): Observable<PropertySubjectId<AppInstalledFull | AppInstalledPreview>[]> {
|
|
||||||
return fromSync$(() => this.appModel.getContents()).pipe(concatMap(apps => {
|
|
||||||
const now = new Date()
|
|
||||||
if (this.appModel.hasLoaded) {
|
|
||||||
return of(apps)
|
|
||||||
} else {
|
|
||||||
return from(this.api.getInstalledApps()).pipe(
|
|
||||||
map(appsRes => {
|
|
||||||
this.appModel.upsertApps(appsRes, now)
|
|
||||||
return this.appModel.getContents()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
appFull (appId: string): Observable<PropertySubject<AppInstalledFull> > {
|
|
||||||
return fromSync$(() => this.appModel.watch(appId)).pipe(
|
|
||||||
concatMap(app => {
|
|
||||||
// if we haven't fetched full, don't return till we do
|
|
||||||
// if we have fetched full, go ahead and return now, but fetch full again in the background
|
|
||||||
if (!app.hasFetchedFull.getValue()) {
|
|
||||||
return from(this.loadInstalledApp(appId))
|
|
||||||
} else {
|
|
||||||
this.loadInstalledApp(appId)
|
|
||||||
return of(app)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
loadInstalledApp (appId: string): Promise<PropertySubject<AppInstalledFull>> {
|
|
||||||
const now = new Date()
|
|
||||||
return this.api.getInstalledApp(appId).then(res => {
|
|
||||||
this.appModel.update({ id: appId, ...res, hasFetchedFull: true }, now)
|
|
||||||
return this.appModel.watch(appId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
server (): Observable<PropertySubject<S9Server>> {
|
|
||||||
return fromSync$(() => this.serverModel.watch()).pipe(concatMap(sw => {
|
|
||||||
if (sw.versionInstalled.getValue()) {
|
|
||||||
return of(sw)
|
|
||||||
} else {
|
|
||||||
console.warn(`server not present, preloading`)
|
|
||||||
return from(this.api.getServer()).pipe(
|
|
||||||
map(res => {
|
|
||||||
this.serverModel.update(res)
|
|
||||||
return this.serverModel.watch()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
364
ui/src/app/models/patch-db/data-model.ts
Normal file
364
ui/src/app/models/patch-db/data-model.ts
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
|
|
||||||
|
export interface DataModel {
|
||||||
|
'server-info': ServerInfo
|
||||||
|
'package-data': { [id: string]: PackageDataEntry }
|
||||||
|
ui: {
|
||||||
|
'server-name': string
|
||||||
|
'welcome-ack': string
|
||||||
|
'auto-check-updates': boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerInfo {
|
||||||
|
id: string
|
||||||
|
version: string
|
||||||
|
'lan-address': URL
|
||||||
|
'tor-address': URL
|
||||||
|
status: ServerStatus
|
||||||
|
registry: URL
|
||||||
|
wifi: WiFiInfo
|
||||||
|
'unread-notification-count': number
|
||||||
|
specs: {
|
||||||
|
CPU: string
|
||||||
|
Disk: string
|
||||||
|
Memory: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ServerStatus {
|
||||||
|
Running = 'running',
|
||||||
|
Updating = 'updating',
|
||||||
|
BackingUp = 'backing-up',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WiFiInfo {
|
||||||
|
ssids: string[]
|
||||||
|
selected: string | null
|
||||||
|
connected: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackageDataEntry {
|
||||||
|
state: PackageState
|
||||||
|
'static-files': {
|
||||||
|
license: URL
|
||||||
|
instructions: URL
|
||||||
|
icon: URL
|
||||||
|
}
|
||||||
|
'temp-manifest'?: Manifest // exists when: installing, updating, removing
|
||||||
|
installed?: InstalledPackageDataEntry, // exists when: installed, updating
|
||||||
|
'install-progress'?: InstallProgress, // exists when: installing, updating
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstallProgress {
|
||||||
|
size: number | null
|
||||||
|
downloaded: number
|
||||||
|
'download-complete': boolean
|
||||||
|
validated: number
|
||||||
|
'validation-complete': boolean
|
||||||
|
read: number
|
||||||
|
'read-complete': boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstalledPackageDataEntry {
|
||||||
|
manifest: Manifest
|
||||||
|
status: Status
|
||||||
|
'interface-info': InterfaceInfo
|
||||||
|
'system-pointers': any[]
|
||||||
|
'current-dependents': { [id: string]: CurrentDependencyInfo }
|
||||||
|
'current-dependencies': { [id: string]: CurrentDependencyInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentDependencyInfo {
|
||||||
|
pointers: any[]
|
||||||
|
'health-checks': string[] // array of health check IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PackageState {
|
||||||
|
Installing = 'installing',
|
||||||
|
Installed = 'installed',
|
||||||
|
Updating = 'updating',
|
||||||
|
Removing = 'removing',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Manifest {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
version: string
|
||||||
|
description: {
|
||||||
|
short: string
|
||||||
|
long: string
|
||||||
|
}
|
||||||
|
'release-notes': string
|
||||||
|
license: string // name
|
||||||
|
'wrapper-repo': URL
|
||||||
|
'upstream-repo': URL
|
||||||
|
'support-site': URL
|
||||||
|
'marketing-site': URL
|
||||||
|
'donation-url': URL | null
|
||||||
|
alerts: {
|
||||||
|
install: string | null
|
||||||
|
uninstall: string | null
|
||||||
|
restore: string | null
|
||||||
|
start: string | null
|
||||||
|
stop: string | null
|
||||||
|
}
|
||||||
|
main: ActionImpl
|
||||||
|
'health-check': ActionImpl
|
||||||
|
config: ConfigActions | null
|
||||||
|
volumes: { [id: string]: Volume }
|
||||||
|
'min-os-version': string
|
||||||
|
interfaces: { [id: string]: InterfaceDef }
|
||||||
|
backup: BackupActions
|
||||||
|
migrations: Migrations
|
||||||
|
actions: { [id: string]: Action }
|
||||||
|
permissions: any // @TODO
|
||||||
|
dependencies: DependencyInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionImpl {
|
||||||
|
type: 'docker'
|
||||||
|
image: string
|
||||||
|
system: boolean
|
||||||
|
entrypoint: string
|
||||||
|
args: string[]
|
||||||
|
mounts: { [id: string]: string }
|
||||||
|
'io-format': DockerIoFormat | null
|
||||||
|
inject: boolean
|
||||||
|
'shm-size': string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DockerIoFormat {
|
||||||
|
Json = 'json',
|
||||||
|
Yaml = 'yaml',
|
||||||
|
Cbor = 'cbor',
|
||||||
|
Toml = 'toml',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigActions {
|
||||||
|
get: ActionImpl
|
||||||
|
set: ActionImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Volume = VolumeData
|
||||||
|
|
||||||
|
export interface VolumeData {
|
||||||
|
type: VolumeType.Data
|
||||||
|
readonly: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumePointer {
|
||||||
|
type: VolumeType.Pointer
|
||||||
|
'package-id': string
|
||||||
|
'volume-id': string
|
||||||
|
path: string
|
||||||
|
readonly: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumeCertificate {
|
||||||
|
type: VolumeType.Certificate
|
||||||
|
'interface-id': string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumeHiddenService {
|
||||||
|
type: VolumeType.HiddenService
|
||||||
|
'interface-id': string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VolumeBackup {
|
||||||
|
type: VolumeType.Backup
|
||||||
|
readonly: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VolumeType {
|
||||||
|
Data = 'data',
|
||||||
|
Pointer = 'pointer',
|
||||||
|
Certificate = 'certificate',
|
||||||
|
HiddenService = 'hidden-service',
|
||||||
|
Backup = 'backup',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterfaceDef {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
ui: boolean
|
||||||
|
'tor-config': TorConfig | null
|
||||||
|
'lan-config': LanConfig | null
|
||||||
|
protocols: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TorConfig {
|
||||||
|
'hidden-service-version': string
|
||||||
|
'port-mapping': { [port: number]: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LanConfig = {
|
||||||
|
[port: number]: { ssl: boolean, mapping: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupActions {
|
||||||
|
create: ActionImpl
|
||||||
|
restore: ActionImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Migrations {
|
||||||
|
from: { [versionRange: string]: ActionImpl }
|
||||||
|
to: { [versionRange: string]: ActionImpl }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
warning: string | null
|
||||||
|
implementation: ActionImpl
|
||||||
|
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
|
||||||
|
'input-spec': ConfigSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Status {
|
||||||
|
configured: boolean
|
||||||
|
main: MainStatus
|
||||||
|
'dependency-errors': { [id: string]: DependencyError }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusRunning | MainStatusBackingUp | MainStatusRestoring
|
||||||
|
|
||||||
|
export interface MainStatusStopped {
|
||||||
|
status: PackageMainStatus.Stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainStatusStopping {
|
||||||
|
status: PackageMainStatus.Stopping
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainStatusRunning {
|
||||||
|
status: PackageMainStatus.Running
|
||||||
|
started: string // UTC date string
|
||||||
|
health: { [id: string]: HealthCheckResult }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainStatusBackingUp {
|
||||||
|
status: PackageMainStatus.BackingUp
|
||||||
|
started: string | null // UTC date string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MainStatusRestoring {
|
||||||
|
status: PackageMainStatus.Restoring
|
||||||
|
running: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PackageMainStatus {
|
||||||
|
Running = 'running',
|
||||||
|
Stopping = 'stopping',
|
||||||
|
Stopped = 'stopped',
|
||||||
|
BackingUp = 'backing-up',
|
||||||
|
Restoring = 'restoring',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HealthCheckResult = HealthCheckResultWarmingUp | HealthCheckResultDisabled | HealthCheckResultSuccess | HealthCheckResultFailure
|
||||||
|
|
||||||
|
export interface HealthCheckResultWarmingUp {
|
||||||
|
time: string // UTC date string
|
||||||
|
result: 'warming-up'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckResultDisabled {
|
||||||
|
time: string // UTC date string
|
||||||
|
result: 'disabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckResultSuccess {
|
||||||
|
time: string // UTC date string
|
||||||
|
result: 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckResultFailure {
|
||||||
|
time: string // UTC date string
|
||||||
|
result: 'failure'
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DependencyError = DependencyErrorNotInstalled | DependencyErrorNotRunning | DependencyErrorIncorrectVersion | DependencyErrorConfigUnsatisfied | DependencyErrorHealthCheckFailed | DependencyErrorInterfaceHealthChecksFailed
|
||||||
|
|
||||||
|
export enum DependencyErrorType {
|
||||||
|
NotInstalled = 'not-installed',
|
||||||
|
NotRunning = 'not-running',
|
||||||
|
IncorrectVersion = 'incorrect-version',
|
||||||
|
ConfigUnsatisfied = 'config-unsatisfied',
|
||||||
|
HealthCheckFailed = 'health-check-failed',
|
||||||
|
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyErrorNotInstalled {
|
||||||
|
type: DependencyErrorType.NotInstalled
|
||||||
|
title: string
|
||||||
|
icon: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyErrorNotRunning {
|
||||||
|
type: DependencyErrorType.NotRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyErrorIncorrectVersion {
|
||||||
|
type: DependencyErrorType.IncorrectVersion
|
||||||
|
expected: string // version range
|
||||||
|
received: string // version
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyErrorConfigUnsatisfied {
|
||||||
|
type: DependencyErrorType.ConfigUnsatisfied
|
||||||
|
errors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyErrorHealthCheckFailed {
|
||||||
|
type: DependencyErrorType.HealthCheckFailed
|
||||||
|
check: HealthCheckResult
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyErrorInterfaceHealthChecksFailed {
|
||||||
|
type: DependencyErrorType.InterfaceHealthChecksFailed
|
||||||
|
failures: { [id: string]: HealthCheckResult }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyInfo {
|
||||||
|
[id: string]: DependencyEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DependencyEntry {
|
||||||
|
version: string
|
||||||
|
optional: string | null
|
||||||
|
recommended: boolean
|
||||||
|
description: string | null
|
||||||
|
config: ConfigRuleEntryWithSuggestions[]
|
||||||
|
interfaces: any[] // @TODO placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigRuleEntryWithSuggestions {
|
||||||
|
rule: string
|
||||||
|
description: string
|
||||||
|
suggestions: Suggestion[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Suggestion {
|
||||||
|
condition: string | null
|
||||||
|
set?: {
|
||||||
|
var: string
|
||||||
|
to?: string
|
||||||
|
'to-value'?: any
|
||||||
|
'to-entropy'?: { charset: string, len: number }
|
||||||
|
}
|
||||||
|
delete?: string
|
||||||
|
push?: {
|
||||||
|
to: string
|
||||||
|
value: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterfaceInfo {
|
||||||
|
ip: string
|
||||||
|
addresses: {
|
||||||
|
[id: string]: { 'tor-address': string, 'lan-address': string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type URL = string
|
||||||
29
ui/src/app/models/patch-db/local-storage-bootstrap.ts
Normal file
29
ui/src/app/models/patch-db/local-storage-bootstrap.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Bootstrapper, DBCache } from 'patch-db-client'
|
||||||
|
import { DataModel } from './data-model'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Storage } from '@ionic/storage'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class LocalStorageBootstrap implements Bootstrapper<DataModel> {
|
||||||
|
static CONTENT_KEY = 'patch-db-cache'
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private readonly storage: Storage,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async init (): Promise<DBCache<DataModel>> {
|
||||||
|
const cache = await this.storage.get(LocalStorageBootstrap.CONTENT_KEY)
|
||||||
|
if (!cache) return { sequence: 0, data: { } as DataModel }
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
async update (cache: DBCache<DataModel>): Promise<void> {
|
||||||
|
return this.storage.set(LocalStorageBootstrap.CONTENT_KEY, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear (): Promise<void> {
|
||||||
|
return this.storage.remove(LocalStorageBootstrap.CONTENT_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
ui/src/app/models/patch-db/patch-db-model.factory.ts
Normal file
24
ui/src/app/models/patch-db/patch-db-model.factory.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { PollSource, Source, WebsocketSource } from 'patch-db-client'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { DataModel } from './data-model'
|
||||||
|
import { LocalStorageBootstrap } from './local-storage-bootstrap'
|
||||||
|
import { PatchDbModel } from './patch-db-model'
|
||||||
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
|
|
||||||
|
export function PatchDbModelFactory (
|
||||||
|
config: ConfigService,
|
||||||
|
bootstrapper: LocalStorageBootstrap,
|
||||||
|
http: ApiService,
|
||||||
|
): PatchDbModel {
|
||||||
|
|
||||||
|
const { patchDb: { usePollOverride, poll, websocket, timeoutForMissingRevision }, isConsulate } = config
|
||||||
|
|
||||||
|
let source: Source<DataModel>
|
||||||
|
if (isConsulate || usePollOverride) {
|
||||||
|
source = new PollSource({ ...poll }, http)
|
||||||
|
} else {
|
||||||
|
source = new WebsocketSource({ ...websocket })
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PatchDbModel({ sources: [source, http], bootstrapper, http, timeoutForMissingRevision })
|
||||||
|
}
|
||||||
52
ui/src/app/models/patch-db/patch-db-model.ts
Normal file
52
ui/src/app/models/patch-db/patch-db-model.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Inject, Injectable, InjectionToken } from '@angular/core'
|
||||||
|
import { PatchDB, PatchDbConfig, Store } from 'patch-db-client'
|
||||||
|
import { Observable, of, Subscription } from 'rxjs'
|
||||||
|
import { catchError, finalize } from 'rxjs/operators'
|
||||||
|
import { DataModel } from './data-model'
|
||||||
|
|
||||||
|
export const PATCH_CONFIG = new InjectionToken<PatchDbConfig<DataModel>>('app.config')
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PatchDbModel {
|
||||||
|
private patchDb: PatchDB<DataModel>
|
||||||
|
private syncSub: Subscription
|
||||||
|
initialized = false
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
@Inject(PATCH_CONFIG) private readonly conf: PatchDbConfig<DataModel>,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async init (): Promise<void> {
|
||||||
|
if (this.patchDb) return console.warn('Cannot re-init patchDbModel')
|
||||||
|
this.patchDb = await PatchDB.init<DataModel>(this.conf)
|
||||||
|
this.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
start (): void {
|
||||||
|
if (this.syncSub) this.stop()
|
||||||
|
this.syncSub = this.patchDb.sync$().subscribe({
|
||||||
|
error: e => console.error('Critical, patch-db-sync sub error', e),
|
||||||
|
complete: () => console.error('Critical, patch-db-sync sub complete'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stop (): void {
|
||||||
|
if (this.syncSub) {
|
||||||
|
this.syncSub.unsubscribe()
|
||||||
|
this.syncSub = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch$: Store<DataModel>['watch$'] = (...args: (string | number)[]): Observable<DataModel> => {
|
||||||
|
// console.log('WATCHING')
|
||||||
|
return this.patchDb.store.watch$(...(args as [])).pipe(
|
||||||
|
catchError(e => {
|
||||||
|
console.error(e)
|
||||||
|
return of(e.message)
|
||||||
|
}),
|
||||||
|
// finalize(() => console.log('unSUBSCRIBing')),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core'
|
|
||||||
import { Subject, BehaviorSubject } from 'rxjs'
|
|
||||||
import { PropertySubject, peekProperties, initPropertySubject } from '../util/property-subject.util'
|
|
||||||
import { AppModel } from './app-model'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { Storage } from '@ionic/storage'
|
|
||||||
import { throttleTime, delay } from 'rxjs/operators'
|
|
||||||
import { StorageKeys } from './storage-keys'
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class ServerModel {
|
|
||||||
lastUpdateTimestamp: Date
|
|
||||||
$delta$ = new Subject<void>()
|
|
||||||
private embassy: PropertySubject<S9Server>
|
|
||||||
|
|
||||||
constructor (
|
|
||||||
private readonly storage: Storage,
|
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
) {
|
|
||||||
this.embassy = this.defaultEmbassy()
|
|
||||||
this.$delta$.pipe(
|
|
||||||
throttleTime(500), delay(500),
|
|
||||||
).subscribe(() => {
|
|
||||||
this.commitCache()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// client fxns
|
|
||||||
watch (): PropertySubject<S9Server> {
|
|
||||||
return this.embassy
|
|
||||||
}
|
|
||||||
|
|
||||||
peek (): S9Server {
|
|
||||||
return peekProperties(this.embassy)
|
|
||||||
}
|
|
||||||
|
|
||||||
update (update: Partial<S9Server>, timestamp: Date = new Date()): void {
|
|
||||||
if (this.lastUpdateTimestamp > timestamp) return
|
|
||||||
|
|
||||||
if (update.versionInstalled && (update.versionInstalled !== this.config.version) && this.embassy.status.getValue() === ServerStatus.RUNNING) {
|
|
||||||
console.log('update detected, force reload page')
|
|
||||||
this.clear()
|
|
||||||
this.nukeCache().then(
|
|
||||||
() => location.replace('?upd=' + new Date()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(update).forEach(
|
|
||||||
([key, value]) => {
|
|
||||||
if (!this.embassy[key]) {
|
|
||||||
console.warn('Received an unexpected key: ', key)
|
|
||||||
this.embassy[key] = new BehaviorSubject(value)
|
|
||||||
} else if (JSON.stringify(this.embassy[key].getValue()) !== JSON.stringify(value)) {
|
|
||||||
this.embassy[key].next(value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
this.$delta$.next()
|
|
||||||
this.lastUpdateTimestamp = timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache mgmt
|
|
||||||
clear () {
|
|
||||||
this.update(peekProperties(this.defaultEmbassy()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private commitCache (): Promise<void> {
|
|
||||||
return this.storage.set(StorageKeys.SERVER_CACHE_KEY, peekProperties(this.embassy))
|
|
||||||
}
|
|
||||||
|
|
||||||
private nukeCache (): Promise<void> {
|
|
||||||
return this.storage.remove(StorageKeys.SERVER_CACHE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
async restoreCache (): Promise<void> {
|
|
||||||
const emb = await this.storage.get(StorageKeys.SERVER_CACHE_KEY)
|
|
||||||
if (emb && emb.versionInstalled === this.config.version) this.update(emb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// server state change
|
|
||||||
markUnreachable (): void {
|
|
||||||
this.update({ status: ServerStatus.UNREACHABLE })
|
|
||||||
this.appModel.markAppsUnreachable()
|
|
||||||
}
|
|
||||||
|
|
||||||
markUnknown (): void {
|
|
||||||
this.update({ status: ServerStatus.UNKNOWN })
|
|
||||||
this.appModel.markAppsUnknown()
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultEmbassy (): PropertySubject<S9Server> {
|
|
||||||
return initPropertySubject({
|
|
||||||
serverId: undefined,
|
|
||||||
name: undefined,
|
|
||||||
origin: this.config.origin,
|
|
||||||
versionInstalled: undefined,
|
|
||||||
versionLatest: undefined,
|
|
||||||
status: ServerStatus.UNKNOWN,
|
|
||||||
badge: 0,
|
|
||||||
alternativeRegistryUrl: undefined,
|
|
||||||
specs: { },
|
|
||||||
wifi: { ssids: [], current: undefined },
|
|
||||||
ssh: [],
|
|
||||||
notifications: [],
|
|
||||||
welcomeAck: true,
|
|
||||||
autoCheckUpdates: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export interface S9Server {
|
|
||||||
serverId: string
|
|
||||||
name: string
|
|
||||||
origin: string
|
|
||||||
versionInstalled: string
|
|
||||||
versionLatest: string | undefined // not on the api as of 0.2.8
|
|
||||||
status: ServerStatus
|
|
||||||
badge: number
|
|
||||||
alternativeRegistryUrl: string | null
|
|
||||||
specs: ServerSpecs
|
|
||||||
wifi: { ssids: string[], current: string }
|
|
||||||
ssh: SSHFingerprint[]
|
|
||||||
notifications: S9Notification[]
|
|
||||||
welcomeAck: boolean
|
|
||||||
autoCheckUpdates: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface S9Notification {
|
|
||||||
id: string
|
|
||||||
appId: string
|
|
||||||
createdAt: string
|
|
||||||
code: string
|
|
||||||
title: string
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerSpecs {
|
|
||||||
[key: string]: string | number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerMetrics {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: {
|
|
||||||
value: string | number | null
|
|
||||||
unit?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSHFingerprint {
|
|
||||||
alg: string
|
|
||||||
hash: string
|
|
||||||
hostname: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiskInfo {
|
|
||||||
logicalname: string,
|
|
||||||
size: string,
|
|
||||||
description: string | null,
|
|
||||||
partitions: DiskPartition[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiskPartition {
|
|
||||||
logicalname: string,
|
|
||||||
isMounted: boolean, // We do not allow backups to mounted partitions
|
|
||||||
size: string | null,
|
|
||||||
label: string | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ServerStatus {
|
|
||||||
UNKNOWN = 'UNKNOWN',
|
|
||||||
UNREACHABLE = 'UNREACHABLE',
|
|
||||||
UPDATING = 'UPDATING',
|
|
||||||
NEEDS_CONFIG = 'NEEDS_CONFIG',
|
|
||||||
RUNNING = 'RUNNING',
|
|
||||||
}
|
|
||||||
@@ -1,52 +1,65 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { EmverComparesPipe, EmverSatisfiesPipe, EmverDisplayPipe, EmverIsValidPipe } from '../pipes/emver.pipe'
|
import { EmverComparesPipe, EmverSatisfiesPipe, EmverDisplayPipe } from '../pipes/emver.pipe'
|
||||||
import { IncludesPipe } from '../pipes/includes.pipe'
|
import { IncludesPipe } from '../pipes/includes.pipe'
|
||||||
import { IconPipe } from '../pipes/icon.pipe'
|
|
||||||
import { TypeofPipe } from '../pipes/typeof.pipe'
|
import { TypeofPipe } from '../pipes/typeof.pipe'
|
||||||
import { MarkdownPipe } from '../pipes/markdown.pipe'
|
import { MarkdownPipe } from '../pipes/markdown.pipe'
|
||||||
import { PeekPropertiesPipe } from '../pipes/peek-properties.pipe'
|
// import { InstalledLatestComparisonPipe, InstalledViewingComparisonPipe } from '../pipes/installed-latest-comparison.pipe'
|
||||||
import { InstalledLatestComparisonPipe, InstalledViewingComparisonPipe } from '../pipes/installed-latest-comparison.pipe'
|
|
||||||
import { AnnotationStatusPipe } from '../pipes/annotation-status.pipe'
|
import { AnnotationStatusPipe } from '../pipes/annotation-status.pipe'
|
||||||
import { TruncateCenterPipe, TruncateEndPipe } from '../pipes/truncate.pipe'
|
import { TruncateCenterPipe, TruncateEndPipe } from '../pipes/truncate.pipe'
|
||||||
import { MaskPipe } from '../pipes/mask.pipe'
|
import { MaskPipe } from '../pipes/mask.pipe'
|
||||||
import { DisplayBulbPipe } from '../pipes/display-bulb.pipe'
|
import { DisplayBulbPipe } from '../pipes/display-bulb.pipe'
|
||||||
|
import { HasUiPipe, LaunchablePipe, ManifestPipe } from '../pipes/ui.pipe'
|
||||||
|
import { EmptyPipe } from '../pipes/empty.pipe'
|
||||||
|
import { StatusPipe } from '../pipes/status.pipe'
|
||||||
|
import { NotificationColorPipe } from '../pipes/notification-color.pipe'
|
||||||
|
import { ReactiveComponentModule } from '@ngrx/component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
EmverComparesPipe,
|
EmverComparesPipe,
|
||||||
EmverSatisfiesPipe,
|
EmverSatisfiesPipe,
|
||||||
TypeofPipe,
|
TypeofPipe,
|
||||||
IconPipe,
|
|
||||||
IncludesPipe,
|
IncludesPipe,
|
||||||
MarkdownPipe,
|
MarkdownPipe,
|
||||||
PeekPropertiesPipe,
|
// InstalledLatestComparisonPipe,
|
||||||
InstalledLatestComparisonPipe,
|
// InstalledViewingComparisonPipe,
|
||||||
InstalledViewingComparisonPipe,
|
|
||||||
AnnotationStatusPipe,
|
AnnotationStatusPipe,
|
||||||
TruncateCenterPipe,
|
TruncateCenterPipe,
|
||||||
TruncateEndPipe,
|
TruncateEndPipe,
|
||||||
MaskPipe,
|
MaskPipe,
|
||||||
DisplayBulbPipe,
|
DisplayBulbPipe,
|
||||||
EmverDisplayPipe,
|
EmverDisplayPipe,
|
||||||
EmverIsValidPipe,
|
HasUiPipe,
|
||||||
|
LaunchablePipe,
|
||||||
|
ManifestPipe,
|
||||||
|
EmptyPipe,
|
||||||
|
StatusPipe,
|
||||||
|
NotificationColorPipe,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
ReactiveComponentModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
EmverComparesPipe,
|
EmverComparesPipe,
|
||||||
EmverSatisfiesPipe,
|
EmverSatisfiesPipe,
|
||||||
TypeofPipe,
|
TypeofPipe,
|
||||||
IconPipe,
|
|
||||||
IncludesPipe,
|
IncludesPipe,
|
||||||
MarkdownPipe,
|
MarkdownPipe,
|
||||||
PeekPropertiesPipe,
|
// InstalledLatestComparisonPipe,
|
||||||
InstalledLatestComparisonPipe,
|
// InstalledViewingComparisonPipe,
|
||||||
AnnotationStatusPipe,
|
AnnotationStatusPipe,
|
||||||
InstalledViewingComparisonPipe,
|
|
||||||
TruncateEndPipe,
|
TruncateEndPipe,
|
||||||
TruncateCenterPipe,
|
TruncateCenterPipe,
|
||||||
MaskPipe,
|
MaskPipe,
|
||||||
DisplayBulbPipe,
|
DisplayBulbPipe,
|
||||||
EmverDisplayPipe,
|
EmverDisplayPipe,
|
||||||
EmverIsValidPipe,
|
HasUiPipe,
|
||||||
|
LaunchablePipe,
|
||||||
|
ManifestPipe,
|
||||||
|
EmptyPipe,
|
||||||
|
StatusPipe,
|
||||||
|
NotificationColorPipe,
|
||||||
|
ReactiveComponentModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SharingModule { }
|
export class SharingModule { }
|
||||||
@@ -8,34 +8,25 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
<ng-container *ngIf="patch.watch$('package-data', pkgId, 'installed') | ngrxPush as installed">
|
||||||
|
<ng-container *ngIf="installed.manifest as manifest">
|
||||||
|
|
||||||
<ng-container *ngIf="!($loading$ | async) && {
|
<ion-item *ngIf="manifest.actions | empty; else actions">
|
||||||
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">
|
<ion-label class="ion-text-wrap">
|
||||||
<p>No Actions for {{ vars.title }} {{ vars.versionInstalled }}.</p>
|
<p>No Actions for {{ manifest.title }} {{ manifest.versionInstalled }}.</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<!-- actions -->
|
<ng-template #actions>
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<ion-item button *ngFor="let action of vars.actions" (click)="handleAction(action)" >
|
<ion-item button *ngFor="let action of manifest.actions | keyvalue: asIsOrder" (click)="handleAction(installed, action)" >
|
||||||
<ion-label class="ion-text-wrap">
|
<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>
|
<h2><ion-text color="primary">{{ action.value.name }}</ion-text><ion-icon *ngIf="!(action.value['allowed-statuses'] | includes: installed.status.main.status)" color="danger" name="close-outline"></ion-icon></h2>
|
||||||
<p><ion-text color="dark">{{ action.description }}</ion-text></p>
|
<p><ion-text color="dark">{{ action.value.description }}</ion-text></p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
@@ -1,49 +1,37 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ApiService, isRpcFailure, isRpcSuccess } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { BehaviorSubject } from 'rxjs'
|
|
||||||
import { AlertController } from '@ionic/angular'
|
import { AlertController } from '@ionic/angular'
|
||||||
import { ModelPreload } from 'src/app/models/model-preload'
|
import { LoaderService } from 'src/app/services/loader.service'
|
||||||
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
|
||||||
import { ServiceAction, AppInstalledFull } from 'src/app/models/app-types'
|
|
||||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
|
||||||
import { map } from 'rxjs/operators'
|
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
|
||||||
import { AppStatus } from 'src/app/models/app-model'
|
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
|
import { Action, InstalledPackageDataEntry, PackageMainStatus } from 'src/app/models/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-actions',
|
selector: 'app-actions',
|
||||||
templateUrl: './app-actions.page.html',
|
templateUrl: './app-actions.page.html',
|
||||||
styleUrls: ['./app-actions.page.scss'],
|
styleUrls: ['./app-actions.page.scss'],
|
||||||
})
|
})
|
||||||
export class AppActionsPage extends Cleanup {
|
export class AppActionsPage {
|
||||||
error = ''
|
pkgId: string
|
||||||
$loading$ = new BehaviorSubject(true)
|
|
||||||
appId: string
|
|
||||||
app: PropertySubject<AppInstalledFull>
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly preload: ModelPreload,
|
|
||||||
private readonly loaderService: LoaderService,
|
private readonly loaderService: LoaderService,
|
||||||
) { super() }
|
public readonly patch: PatchDbModel,
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.appId = this.route.snapshot.paramMap.get('appId')
|
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||||
|
|
||||||
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) {
|
async handleAction (pkg: InstalledPackageDataEntry, action: { key: string, value: Action }) {
|
||||||
if (action.allowedStatuses.includes(this.app.status.getValue())) {
|
if ((action.value['allowed-statuses'] as PackageMainStatus[]).includes(pkg.status.main.status)) {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
message: `Are you sure you want to execute action "${action.name}"? ${action.warning ? action.warning : ""}`,
|
message: `Are you sure you want to execute action "${action.value.name}"? ${action.value.warning || ''}`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
@@ -52,7 +40,7 @@ export class AppActionsPage extends Cleanup {
|
|||||||
{
|
{
|
||||||
text: 'Execute',
|
text: 'Execute',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.executeAction(action)
|
this.executeAction(pkg.manifest.id, action.key)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -83,25 +71,19 @@ export class AppActionsPage extends Cleanup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async executeAction(action: ServiceAction) {
|
private async executeAction (pkgId: string, actionId: string) {
|
||||||
try {
|
try {
|
||||||
const res = await this.loaderService.displayDuringP(
|
const res = await this.loaderService.displayDuringP(
|
||||||
this.apiService.serviceAction(this.appId, action),
|
this.apiService.executePackageAction({ id: pkgId, 'action-id': actionId }),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isRpcFailure(res)) {
|
|
||||||
this.presentAlertActionFail(res.error.code, res.error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRpcSuccess(res)) {
|
|
||||||
const successAlert = await this.alertCtrl.create({
|
const successAlert = await this.alertCtrl.create({
|
||||||
header: 'Execution Complete',
|
header: 'Execution Complete',
|
||||||
message: res.result.split('\n').join('</br ></br />'),
|
message: res.message.split('\n').join('</br ></br />'),
|
||||||
buttons: ['OK'],
|
buttons: ['OK'],
|
||||||
cssClass: 'alert-success-message',
|
cssClass: 'alert-success-message',
|
||||||
})
|
})
|
||||||
return await successAlert.present()
|
return await successAlert.present()
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof HttpErrorResponse) {
|
if (e instanceof HttpErrorResponse) {
|
||||||
this.presentAlertActionFail(e.status, e.message)
|
this.presentAlertActionFail(e.status, e.message)
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { Routes, RouterModule } from '@angular/router'
|
|||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { AppAvailableListPage } from './app-available-list.page'
|
import { AppAvailableListPage } from './app-available-list.page'
|
||||||
import { SharingModule } from '../../../modules/sharing.module'
|
import { SharingModule } from '../../../modules/sharing.module'
|
||||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
|
||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||||
import { UpdateOsBannerComponentModule } from 'src/app/components/update-os-banner/update-os-banner.component.module'
|
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -24,9 +22,7 @@ const routes: Routes = [
|
|||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
StatusComponentModule,
|
StatusComponentModule,
|
||||||
SharingModule,
|
SharingModule,
|
||||||
PwaBackComponentModule,
|
|
||||||
BadgeMenuComponentModule,
|
BadgeMenuComponentModule,
|
||||||
UpdateOsBannerComponentModule,
|
|
||||||
],
|
],
|
||||||
declarations: [AppAvailableListPage],
|
declarations: [AppAvailableListPage],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,65 +1,77 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title>Service Marketplace</ion-title>
|
<ion-title>Embassy Marketplace</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<badge-menu-button></badge-menu-button>
|
<badge-menu-button></badge-menu-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
<update-os-banner></update-os-banner>
|
<ion-toolbar *ngIf="!pageLoading">
|
||||||
|
<ion-searchbar (ionChange)="search($event)" debounce="400"></ion-searchbar>
|
||||||
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-bottom">
|
<ion-content class="ion-padding-top" *ngrxLet="patch.watch$('package-data') as installedPkgs">
|
||||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
<ion-spinner *ngIf="pageLoading; else pageLoaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
|
||||||
</ion-refresher>
|
|
||||||
|
|
||||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
<ng-template #pageLoaded>
|
||||||
|
|
||||||
<ng-container *ngIf="!($loading$ | async)">
|
|
||||||
|
|
||||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
|
<div class="scrollable">
|
||||||
<ion-card-header>
|
<ion-button
|
||||||
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
|
*ngFor="let cat of data.categories"
|
||||||
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
size="small"
|
||||||
</ion-card-header>
|
fill="clear"
|
||||||
<ion-card-content>
|
[color]="cat === category ? 'success' : 'dark'"
|
||||||
<b>Get ready. View the update instructions.</b>
|
(click)="switchCategory(cat)"
|
||||||
</ion-card-content>
|
>
|
||||||
</ion-card>
|
{{ cat }}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
|
<ion-card *ngIf="eos && category === 'featured'" class="eos-card" (click)="updateEos()">
|
||||||
<ion-card-header>
|
<ion-card-header>
|
||||||
<ion-card-subtitle>Now Available...</ion-card-subtitle>
|
<ion-card-subtitle>Now Available...</ion-card-subtitle>
|
||||||
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
<ion-card-title>EmbassyOS Version {{ eos.version }}</ion-card-title>
|
||||||
</ion-card-header>
|
</ion-card-header>
|
||||||
<ion-card-content>
|
<ion-card-content>
|
||||||
<b>View the update instructions.</b>
|
{{ eos.headline }}
|
||||||
</ion-card-content>
|
</ion-card-content>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
|
<ion-spinner *ngIf="pkgsLoading; else pkgsLoaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||||
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
|
|
||||||
|
<ng-template #pkgsLoaded>
|
||||||
|
<ion-card *ngFor="let pkg of pkgs" style="margin: 10px 10px;" [routerLink]="[pkg.id]">
|
||||||
|
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;">
|
||||||
<ion-avatar style="margin-top: 8px;" slot="start">
|
<ion-avatar style="margin-top: 8px;" slot="start">
|
||||||
<img [src]="app.subject.iconURL | async | iconParse" />
|
<img [src]="pkg.icon" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label style="margin-top: 6px; margin-bottom: 3px">
|
<ion-label style="margin-top: 6px; margin-bottom: 3px">
|
||||||
<h1 style="font-family: 'Montserrat'; font-size: 20px; margin-bottom: 0px;">
|
<h1 style="font-family: 'Montserrat'; font-size: 20px; margin-bottom: 0px;">
|
||||||
{{app.subject.title | async}}
|
{{ pkg.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div *ngIf="!l.installing && l.installComparison === 'installed-equal'" class="beneath-title">
|
<p>{{ pkg.version }}</p>
|
||||||
<ion-text style="font-size: 12px;" color="success">Installed</ion-text>
|
<div class="beneath-title" *ngIf="installedPkgs[pkg.id] as pkgI">
|
||||||
</div>
|
<ng-container *ngIf="pkgI.state === PackageState.Installed">
|
||||||
<div *ngIf="!l.installing && l.installComparison === 'installed-below'" class="beneath-title">
|
<ion-text *ngIf="(pkg.version | compareEmver : pkgI.installed.manifest.version) === 0" style="font-size: 12px;" color="success">Installed</ion-text>
|
||||||
<ion-text style="font-size: 12px;" color="warning">Update Available</ion-text>
|
<ion-text *ngIf="(pkg.version | compareEmver : pkgI.installed.manifest.version) === 1" style="font-size: 12px;" color="warning">Update Available</ion-text>
|
||||||
</div>
|
</ng-container>
|
||||||
<div *ngIf="l.installing" class="beneath-title" style="display: flex; flex-direction: row; align-items: center;">
|
<div class="beneath-title" *ngIf="pkgI.state === PackageState.Installing" style="display: flex; flex-direction: row; align-items: center;">
|
||||||
<ion-text style="font-size: 12px;" color="primary">Installing</ion-text>
|
<ion-text style="font-size: 12px;" color="primary">Installing</ion-text>
|
||||||
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
|
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="beneath-title" *ngIf="pkgI.state === PackageState.Updating" style="display: flex; flex-direction: row; align-items: center;">
|
||||||
|
<ion-text style="font-size: 12px;" color="primary">Updating</ion-text>
|
||||||
|
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
|
||||||
|
</div>
|
||||||
|
<div class="beneath-title" *ngIf="pkgI.state === PackageState.Removing" style="display: flex; flex-direction: row; align-items: center;">
|
||||||
|
<ion-text style="font-size: 12px;" color="danger">Removing</ion-text>
|
||||||
|
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="danger"></ion-spinner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-card-content style="
|
<ion-card-content style="
|
||||||
@@ -67,8 +79,9 @@
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
">
|
">
|
||||||
{{ app.subject.descriptionShort | async }}
|
{{ pkg.descriptionShort }}
|
||||||
</ion-card-content>
|
</ion-card-content>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
</ng-container>
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -5,12 +5,24 @@
|
|||||||
padding: 1px 0px 1.5px 0px;
|
padding: 1px 0px 1.5px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructions-card {
|
.scrollable {
|
||||||
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
|
overflow: auto;
|
||||||
margin: 16px 10px;
|
white-space: nowrap;
|
||||||
|
background-color: var(--ion-color-light);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.available-card {
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.eos-card {
|
||||||
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
|
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
|
||||||
margin: 16px 10px;
|
margin: 16px 10px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import { Component, NgZone } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { AppModel } from 'src/app/models/app-model'
|
import { MarketplaceData, MarketplaceEOS, AvailablePreview } from 'src/app/services/api/api-types'
|
||||||
import { AppAvailablePreview, AppInstalledPreview } from 'src/app/models/app-types'
|
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { pauseFor } from 'src/app/util/misc.util'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { PropertySubjectId, initPropertySubject } from 'src/app/util/property-subject.util'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
import { take } from 'rxjs/operators'
|
import { PackageState } from 'src/app/models/patch-db/data-model'
|
||||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
|
||||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
|
||||||
import { V1Status } from 'src/app/services/api/api-types'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-available-list',
|
selector: 'app-available-list',
|
||||||
@@ -16,83 +13,93 @@ import { V1Status } from 'src/app/services/api/api-types'
|
|||||||
styleUrls: ['./app-available-list.page.scss'],
|
styleUrls: ['./app-available-list.page.scss'],
|
||||||
})
|
})
|
||||||
export class AppAvailableListPage {
|
export class AppAvailableListPage {
|
||||||
$loading$ = new BehaviorSubject(true)
|
pageLoading = true
|
||||||
|
pkgsLoading = true
|
||||||
error = ''
|
error = ''
|
||||||
installedAppDeltaSubscription: Subscription
|
|
||||||
apps: PropertySubjectId<AppAvailablePreview>[] = []
|
category = 'featured'
|
||||||
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
|
query: string
|
||||||
v1Status: V1Status = { status: 'nothing', version: '' }
|
|
||||||
|
data: MarketplaceData
|
||||||
|
eos: MarketplaceEOS
|
||||||
|
pkgs: AvailablePreview[] = []
|
||||||
|
|
||||||
|
PackageState = PackageState
|
||||||
|
|
||||||
|
page = 1
|
||||||
|
needInfinite = false
|
||||||
|
readonly perPage = 20
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly appModel: AppModel,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly zone: NgZone,
|
private readonly wizardBaker: WizardBaker,
|
||||||
private readonly osUpdateService: OsUpdateService,
|
public patch: PatchDbModel,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
this.installedAppDeltaSubscription = this.appModel
|
|
||||||
.watchDelta('update')
|
|
||||||
.subscribe(({ id }) => this.mergeInstalledProps(id))
|
|
||||||
|
|
||||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
|
||||||
this.getApps(),
|
|
||||||
this.checkV1Status(),
|
|
||||||
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
|
|
||||||
pauseFor(600),
|
|
||||||
]))
|
|
||||||
}
|
|
||||||
|
|
||||||
ionViewDidEnter () {
|
|
||||||
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkV1Status () {
|
|
||||||
try {
|
try {
|
||||||
this.v1Status = await this.apiService.checkV1Status()
|
const [data, eos, pkgs] = await Promise.all([
|
||||||
} catch (e) {
|
this.apiService.getMarketplaceData({ }),
|
||||||
console.error(e)
|
this.apiService.getEos({ }),
|
||||||
}
|
this.getPkgs(),
|
||||||
}
|
|
||||||
|
|
||||||
mergeInstalledProps (appInstalledId: string) {
|
|
||||||
const appAvailable = this.apps.find(app => app.id === appInstalledId)
|
|
||||||
if (!appAvailable) return
|
|
||||||
|
|
||||||
const app = this.appModel.watch(appInstalledId)
|
|
||||||
combineLatest([app.status, app.versionInstalled])
|
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe(([status, versionInstalled]) => {
|
|
||||||
this.zone.run(() => {
|
|
||||||
appAvailable.subject.status.next(status)
|
|
||||||
appAvailable.subject.versionInstalled.next(versionInstalled)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy () {
|
|
||||||
this.installedAppDeltaSubscription.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
async doRefresh (e: any) {
|
|
||||||
await Promise.all([
|
|
||||||
this.getApps(),
|
|
||||||
pauseFor(600),
|
|
||||||
])
|
])
|
||||||
e.target.complete()
|
this.data = data
|
||||||
}
|
this.eos = eos
|
||||||
|
this.pkgs = pkgs
|
||||||
async getApps (): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.apps = await this.apiService.getAvailableApps().then(apps =>
|
|
||||||
apps
|
|
||||||
.sort( (a1, a2) => a2.latestVersionTimestamp.getTime() - a1.latestVersionTimestamp.getTime())
|
|
||||||
.map(a => ({ id: a.id, subject: initPropertySubject(a) })),
|
|
||||||
)
|
|
||||||
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
this.error = e.message
|
this.error = e.message
|
||||||
|
} finally {
|
||||||
|
this.pageLoading = false
|
||||||
|
this.pkgsLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doInfinite (e: any): Promise<void> {
|
||||||
|
const pkgs = await this.getPkgs()
|
||||||
|
this.pkgs = this.pkgs.concat(pkgs)
|
||||||
|
e.target.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
async search (e?: any): Promise<void> {
|
||||||
|
this.query = e.target.value || undefined
|
||||||
|
this.page = 1
|
||||||
|
this.pkgs = await this.getPkgs()
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEos (): Promise<void> {
|
||||||
|
await wizardModal(
|
||||||
|
this.modalCtrl,
|
||||||
|
this.wizardBaker.updateOS({
|
||||||
|
version: this.eos.version,
|
||||||
|
releaseNotes: this.eos.notes,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPkgs (): Promise<AvailablePreview[]> {
|
||||||
|
this.pkgsLoading = true
|
||||||
|
try {
|
||||||
|
const pkgs = await this.apiService.getAvailableList({
|
||||||
|
category: this.category,
|
||||||
|
query: this.query,
|
||||||
|
page: this.page,
|
||||||
|
'per-page': this.perPage,
|
||||||
|
})
|
||||||
|
this.needInfinite = pkgs.length >= this.perPage
|
||||||
|
this.page++
|
||||||
|
return pkgs
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.error = e.message
|
||||||
|
} finally {
|
||||||
|
this.pkgsLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchCategory (category: string): Promise<void> {
|
||||||
|
this.category = category
|
||||||
|
this.pkgs = await this.getPkgs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { DependencyListComponentModule } from '../../../components/dependency-list/dependency-list.component.module'
|
|
||||||
import { AppAvailableShowPage } from './app-available-show.page'
|
import { AppAvailableShowPage } from './app-available-show.page'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||||
@@ -10,7 +9,6 @@ import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/b
|
|||||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||||
import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module'
|
import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module'
|
||||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||||
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
|
|
||||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -25,14 +23,12 @@ const routes: Routes = [
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
StatusComponentModule,
|
StatusComponentModule,
|
||||||
DependencyListComponentModule,
|
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
SharingModule,
|
SharingModule,
|
||||||
PwaBackComponentModule,
|
PwaBackComponentModule,
|
||||||
RecommendationButtonComponentModule,
|
RecommendationButtonComponentModule,
|
||||||
BadgeMenuComponentModule,
|
BadgeMenuComponentModule,
|
||||||
InstallWizardComponentModule,
|
InstallWizardComponentModule,
|
||||||
ErrorMessageComponentModule,
|
|
||||||
InformationPopoverComponentModule,
|
InformationPopoverComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [AppAvailableShowPage],
|
declarations: [AppAvailableShowPage],
|
||||||
|
|||||||
@@ -10,109 +10,140 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-bottom" *ngIf="{
|
<ion-content class="ion-padding-bottom">
|
||||||
id: $app$.id | async,
|
|
||||||
status: $app$.status | async,
|
|
||||||
title: $app$.title | async,
|
|
||||||
versionInstalled: $app$.versionInstalled | async,
|
|
||||||
versionViewing: $app$.versionViewing | async,
|
|
||||||
descriptionLong: $app$.descriptionLong | async,
|
|
||||||
licenseName: $app$.licenseName | async,
|
|
||||||
licenseLink: $app$.licenseLink | async,
|
|
||||||
serviceRequirements: $app$.serviceRequirements | async,
|
|
||||||
iconURL: $app$.iconURL | async,
|
|
||||||
releaseNotes: $app$.releaseNotes | async
|
|
||||||
} as vars"
|
|
||||||
>
|
|
||||||
<ion-spinner *ngIf="($loading$ | async)" class="center" name="lines" color="warning"></ion-spinner>
|
|
||||||
|
|
||||||
<error-message [$error$]="$error$" [dismissable]="vars.id"></error-message>
|
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||||
|
|
||||||
<ng-container *ngIf="!($loading$ | async) && vars.id && ($app$ | compareInstalledAndViewing | async) as installedStatus">
|
<ng-template #loaded>
|
||||||
|
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||||
|
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ng-container *ngrxLet="patch.watch$('package-data', pkgId) as localPkg">
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<ion-item lines="none">
|
<ion-item lines="none">
|
||||||
<ion-avatar slot="start">
|
<ion-avatar slot="start">
|
||||||
<img [src]="vars.iconURL | iconParse" />
|
<img [src]="pkg.icon" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<h1 style="font-family: 'Montserrat'">{{ vars.title }}</h1>
|
<h1 style="font-family: 'Montserrat'">{{ pkg.manifest.title }}</h1>
|
||||||
<h3>{{ vars.versionViewing | displayEmver }}</h3>
|
<h3>{{ pkg.manifest.version | displayEmver }}</h3>
|
||||||
<ng-container *ngIf="vars.status !== 'INSTALLING'">
|
<!-- no localPkg -->
|
||||||
<h3 *ngIf="installedStatus === 'installed-equal'"><ion-text color="medium">Installed</ion-text></h3>
|
<h3 *ngIf="!localPkg; else local">
|
||||||
<h3 *ngIf="installedStatus === 'installed-below' || installedStatus === 'installed-above'"><ion-text color="medium">Installed </ion-text><ion-text style="font-size: small" color="medium"> at {{vars.versionInstalled | displayEmver}}</ion-text></h3>
|
<ion-text color="medium">Not Installed</ion-text>
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="vars.status === 'INSTALLING'">
|
|
||||||
<h3>
|
|
||||||
<status appStatus="INSTALLING" [text]="' (' + (vars.versionInstalled | displayEmver) + ')'" size="medium"></status>
|
|
||||||
</h3>
|
</h3>
|
||||||
</ng-container>
|
<!-- localPkg -->
|
||||||
|
<ng-template #local>
|
||||||
|
<h3 *ngIf="localPkg.state !== PackageState.Installed; else installed">
|
||||||
|
<!-- installing, updating, removing -->
|
||||||
|
<ion-text [color]="localPkg.state === PackageState.Removing ? 'danger' : 'primary'">{{ localPkg.state }}</ion-text>
|
||||||
|
<ion-spinner class="dots dots-medium" name="dots" [color]="localPkg.state === PackageState.Removing ? 'danger' : 'primary'"></ion-spinner>
|
||||||
|
</h3>
|
||||||
|
<!-- installed -->
|
||||||
|
<ng-template #installed>
|
||||||
|
<h3>
|
||||||
|
<ion-text color="medium">Installed at {{ localPkg.installed.manifest.version | displayEmver }}</ion-text>
|
||||||
|
</h3>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|
||||||
<ion-button *ngIf="!vars.versionInstalled" class="main-action-button" expand="block" fill="outline" color="success" (click)="install()">
|
<!-- no localPkg -->
|
||||||
|
<ion-button *ngIf="!localPkg; else localPkg2" class="main-action-button" expand="block" fill="outline" color="success" (click)="install()">
|
||||||
Install
|
Install
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<div *ngIf="vars.versionInstalled">
|
<!-- localPkg -->
|
||||||
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', vars.id]">
|
<ng-template #localPkg2>
|
||||||
|
<!-- not removing -->
|
||||||
|
<ng-container *ngIf="localPkg.state !== PackageState.Removing">
|
||||||
|
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', pkgId]">
|
||||||
Go to Service
|
Go to Service
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<div *ngIf="vars.status !== 'INSTALLING' ">
|
<!-- not installing or updating -->
|
||||||
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
||||||
Update to {{ vars.versionViewing | displayEmver }}
|
<ion-button *ngIf="(localPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === -1" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||||
|
Update to {{ pkg.manifest.version | displayEmver }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
<ion-button *ngIf="(localPkg.installed.manifest.version | compareEmver : pkg.manifest.version) === 1" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||||
Downgrade to {{ vars.versionViewing | displayEmver }}
|
Downgrade to {{ pkg.manifest.version | displayEmver }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</ng-container>
|
||||||
</div>
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ion-item-group>
|
<!-- recommendation -->
|
||||||
<ng-container *ngIf="recommendation">
|
<ion-item *ngIf="rec && showRec" class="rec-item">
|
||||||
<ion-item class="recommendation-item">
|
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<h2 style="display: flex; align-items: center;">
|
<h2 style="display: flex; align-items: center;">
|
||||||
<ion-avatar style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
<ion-avatar style="height: 3vh; width: 3vh; margin: 5px" slot="start">
|
||||||
<img [src]="recommendation.iconURL | iconParse" [alt]="recommendation.title"/>
|
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{recommendation.title}}</ion-text>
|
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
<div style="margin: 2px 5px">
|
<div style="margin: 7px 5px;">
|
||||||
<p style="color: var(--ion-color-medium); font-size: small">{{recommendation.description}}</p>
|
<p style="color: var(--ion-color-dark); font-size: small">{{ rec.description }}</p>
|
||||||
<p *ngIf="vars.versionViewing | satisfiesEmver: recommendation.versionSpec" class="recommendation-text">{{vars.title}} version {{vars.versionViewing | displayEmver}} is compatible.</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="!(vars.versionViewing | satisfiesEmver: recommendation.versionSpec)" class="recommendation-text recommendation-error">{{vars.title}} version {{vars.versionViewing | displayEmver}} is NOT compatible.</p>
|
<p *ngIf="!(pkg.manifest.version | satisfiesEmver: rec.version)" class="recommendation-text recommendation-error">{{ pkg.manifest.title }} version {{ pkg.manifest.version | displayEmver }} is NOT compatible.</p>
|
||||||
|
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||||
|
<ion-icon name="close-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ vars.versionViewing | displayEmver }}</ion-item-divider>
|
<ion-item-group>
|
||||||
|
<!-- release notes -->
|
||||||
|
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ pkg.manifest.version | displayEmver }}</ion-item-divider>
|
||||||
<ion-item lines="none">
|
<ion-item lines="none">
|
||||||
<ion-label *ngIf="!($newVersionLoading$ | async)" style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
|
<ion-label style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
|
||||||
<div id='release-notes'color="dark" [innerHTML]="vars.releaseNotes | markdown"></div>
|
<div id='release-notes' color="dark" [innerHTML]="pkg.manifest['release-notes'] | markdown"></div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<!-- description -->
|
||||||
<ion-item-divider class="divider">Description</ion-item-divider>
|
<ion-item-divider class="divider">
|
||||||
|
<ion-text color="dark">Description</ion-text>
|
||||||
|
</ion-item-divider>
|
||||||
<ion-item lines="none">
|
<ion-item lines="none">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<ion-text color="medium">
|
<ion-text color="dark">
|
||||||
<h5>{{ vars.descriptionLong }}</h5>
|
<h5>{{ pkg.manifest.description.long }}</h5>
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<!-- dependencies -->
|
||||||
<ng-container *ngIf="(vars.serviceRequirements)?.length">
|
<ng-container *ngIf="!(pkg.manifest.dependencies | empty)">
|
||||||
<ion-item-divider class="divider">Service Dependencies
|
<ion-item-divider class="divider">
|
||||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(serviceDependencyDefintion, $event)">
|
<ion-text color="dark">Service Dependencies</ion-text>
|
||||||
|
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="dark" (click)="presentPopover(depDefintion, $event)">
|
||||||
<ion-icon name="help-circle-outline"></ion-icon>
|
<ion-icon name="help-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
|
|
||||||
|
<div *ngFor="let dep of pkg.manifest.dependencies | keyvalue">
|
||||||
|
<ion-item *ngIf="!dep.value.optional" class="dependency-item">
|
||||||
|
<ion-avatar slot="start">
|
||||||
|
<img [src]="pkg['dependency-metadata'][dep.key].icon" />
|
||||||
|
</ion-avatar>
|
||||||
|
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
|
||||||
|
<h4 style="font-family: 'Montserrat'">
|
||||||
|
{{ pkg['dependency-metadata'][dep.key].title }}
|
||||||
|
<span *ngIf="dep.value.recommended" style="font-family: 'Open Sans'; font-size: small; color: var(--ion-color-dark)"> (recommended)</span>
|
||||||
|
</h4>
|
||||||
|
<p style="font-size: small">{{ dep.value.version | displayEmver }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item style="margin-bottom: 10px" *ngIf="dep.value.description" lines="none">
|
||||||
|
<div style="font-size: small; color: var(--ion-color-dark)" [innerHtml]="dep.value.description"></div>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- versions -->
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||||
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
|
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
|
||||||
@@ -120,9 +151,10 @@
|
|||||||
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item lines="none" button (click)="presentAlertVersions()">
|
<ion-item lines="none" button (click)="presentAlertVersions()">
|
||||||
<ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
|
<ion-icon color="dark" slot="start" name="file-tray-stacked-outline"></ion-icon>
|
||||||
<ion-label>Other versions</ion-label>
|
<ion-label color="dark">Other versions</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-container>
|
|
||||||
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
@@ -1,81 +1,64 @@
|
|||||||
import { Component, NgZone } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { AppAvailableFull, AppAvailableVersionSpecificInfo } from 'src/app/models/app-types'
|
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular'
|
import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular'
|
||||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
|
||||||
import { BehaviorSubject, from, Observable, of } from 'rxjs'
|
|
||||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
|
||||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||||
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 { AppModel } from 'src/app/models/app-model'
|
|
||||||
import { initPropertySubject, peekProperties, PropertySubject } from 'src/app/util/property-subject.util'
|
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
|
||||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
import { 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'
|
||||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||||
import { pauseFor } from 'src/app/util/misc.util'
|
import { pauseFor } from 'src/app/util/misc.util'
|
||||||
|
import { AvailableShow } from 'src/app/services/api/api-types'
|
||||||
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
|
import { PackageState } from 'src/app/models/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-available-show',
|
selector: 'app-available-show',
|
||||||
templateUrl: './app-available-show.page.html',
|
templateUrl: './app-available-show.page.html',
|
||||||
styleUrls: ['./app-available-show.page.scss'],
|
styleUrls: ['./app-available-show.page.scss'],
|
||||||
})
|
})
|
||||||
export class AppAvailableShowPage extends Cleanup {
|
export class AppAvailableShowPage {
|
||||||
$loading$ = new BehaviorSubject(true)
|
loading = true
|
||||||
|
error = ''
|
||||||
|
pkg: AvailableShow
|
||||||
|
pkgId: string
|
||||||
|
|
||||||
// When a new version is selected
|
PackageState = PackageState
|
||||||
$newVersionLoading$ = new BehaviorSubject(false)
|
|
||||||
// When dependencies are refreshing
|
|
||||||
$dependenciesLoading$ = new BehaviorSubject(false)
|
|
||||||
|
|
||||||
$error$ = new BehaviorSubject(undefined)
|
rec: Recommendation | null = null
|
||||||
$app$: PropertySubject<AppAvailableFull> = { } as any
|
showRec = true
|
||||||
appId: string
|
|
||||||
|
|
||||||
openRecommendation = false
|
depDefinition = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||||
recommendation: Recommendation | null = null
|
|
||||||
|
|
||||||
serviceDependencyDefintion = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly zone: NgZone,
|
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly wizardBaker: WizardBaker,
|
private readonly wizardBaker: WizardBaker,
|
||||||
private readonly navCtrl: NavController,
|
private readonly navCtrl: NavController,
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly popoverController: PopoverController,
|
private readonly popoverController: PopoverController,
|
||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
) {
|
public readonly patch: PatchDbModel,
|
||||||
super()
|
) { }
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
this.pkgId = this.route.snapshot.paramMap.get('pkgId') as string
|
||||||
|
this.rec = history.state && history.state.installRec as Recommendation
|
||||||
this.cleanup(
|
this.getPkg()
|
||||||
// new version always includes dependencies, but not vice versa
|
|
||||||
this.$newVersionLoading$.subscribe(this.$dependenciesLoading$),
|
|
||||||
markAsLoadingDuring$(this.$loading$,
|
|
||||||
from(this.apiService.getAvailableApp(this.appId)).pipe(
|
|
||||||
tap(app => this.$app$ = initPropertySubject(app)),
|
|
||||||
concatMap(() => this.fetchRecommendation()),
|
|
||||||
),
|
|
||||||
).pipe(
|
|
||||||
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
|
|
||||||
catchError(e => of(this.setError(e))),
|
|
||||||
).subscribe(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidEnter () {
|
async getPkg (version?: string): Promise<void> {
|
||||||
markAsLoadingDuring$(this.$dependenciesLoading$, this.syncVersionSpecificInfo()).subscribe({
|
this.loading = true
|
||||||
error: e => this.setError(e),
|
try {
|
||||||
})
|
this.pkg = await this.apiService.getAvailableShow({ id: this.pkgId, version })
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.error = e.message
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentPopover (information: string, ev: any) {
|
async presentPopover (information: string, ev: any) {
|
||||||
@@ -92,34 +75,17 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
return await popover.present()
|
return await popover.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
syncVersionSpecificInfo (versionSpec?: string): Observable<any> {
|
|
||||||
if (!this.$app$.versionViewing) return of({ })
|
|
||||||
const specToFetch = versionSpec || `=${this.$app$.versionViewing.getValue()}`
|
|
||||||
return from(this.apiService.getAvailableAppVersionSpecificInfo(this.appId, specToFetch)).pipe(
|
|
||||||
tap(versionInfo => this.mergeInfo(versionInfo)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeInfo (versionSpecificInfo: AppAvailableVersionSpecificInfo) {
|
|
||||||
this.zone.run(() => {
|
|
||||||
Object.entries(versionSpecificInfo).forEach( ([k, v]) => {
|
|
||||||
if (!this.$app$[k]) this.$app$[k] = new BehaviorSubject(undefined)
|
|
||||||
if (v !== this.$app$[k].getValue()) this.$app$[k].next(v)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentAlertVersions () {
|
async presentAlertVersions () {
|
||||||
const app = peekProperties(this.$app$)
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Versions',
|
header: 'Versions',
|
||||||
backdropDismiss: false,
|
backdropDismiss: false,
|
||||||
inputs: app.versions.sort((a, b) => -1 * this.emver.compare(a, b)).map(v => {
|
inputs: this.pkg.versions.sort((a, b) => -1 * this.emver.compare(a, b)).map(v => {
|
||||||
return { name: v, // for CSS
|
return {
|
||||||
|
name: v, // for CSS
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
label: displayEmver(v), // appearance on screen
|
label: displayEmver(v), // appearance on screen
|
||||||
value: v, // literal SEM version value
|
value: v, // literal SEM version value
|
||||||
checked: app.versionViewing === v,
|
checked: this.pkg.manifest.version === v,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -129,17 +95,7 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
}, {
|
}, {
|
||||||
text: 'Ok',
|
text: 'Ok',
|
||||||
handler: (version: string) => {
|
handler: (version: string) => {
|
||||||
const previousVersion = this.$app$.versionViewing.getValue()
|
this.getPkg(version)
|
||||||
this.$app$.versionViewing.next(version)
|
|
||||||
markAsLoadingDuring$(
|
|
||||||
this.$newVersionLoading$, this.syncVersionSpecificInfo(`=${version}`),
|
|
||||||
)
|
|
||||||
.subscribe({
|
|
||||||
error: e => {
|
|
||||||
this.setError(e)
|
|
||||||
this.$app$.versionViewing.next(previousVersion)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -149,15 +105,14 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async install () {
|
async install () {
|
||||||
const app = peekProperties(this.$app$)
|
const { id, title, version, dependencies, alerts } = this.pkg.manifest
|
||||||
const { cancelled } = await wizardModal(
|
const { cancelled } = await wizardModal(
|
||||||
this.modalCtrl,
|
this.modalCtrl,
|
||||||
this.wizardBaker.install({
|
this.wizardBaker.install({
|
||||||
id: app.id,
|
id,
|
||||||
title: app.title,
|
title,
|
||||||
version: app.versionViewing,
|
version,
|
||||||
serviceRequirements: app.serviceRequirements,
|
installAlert: alerts.install,
|
||||||
installAlert: app.installAlert,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
@@ -166,14 +121,13 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update (action: 'update' | 'downgrade') {
|
async update (action: 'update' | 'downgrade') {
|
||||||
const app = peekProperties(this.$app$)
|
const { id, title, version, dependencies, alerts } = this.pkg.manifest
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
id: app.id,
|
id,
|
||||||
title: app.title,
|
title,
|
||||||
version: app.versionViewing,
|
version,
|
||||||
serviceRequirements: app.serviceRequirements,
|
serviceRequirements: dependencies,
|
||||||
installAlert: app.installAlert,
|
installAlert: alerts.install,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cancelled } = await wizardModal(
|
const { cancelled } = await wizardModal(
|
||||||
@@ -188,27 +142,7 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
this.navCtrl.back()
|
this.navCtrl.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchRecommendation (): Observable<any> {
|
dismissRec () {
|
||||||
this.recommendation = history.state && history.state.installationRecommendation
|
this.showRec = false
|
||||||
|
|
||||||
if (this.recommendation) {
|
|
||||||
return from(this.syncVersionSpecificInfo(this.recommendation.versionSpec))
|
|
||||||
} else {
|
|
||||||
return of({ })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncWhenDependencyInstalls (): Observable<void> {
|
|
||||||
return this.$app$.serviceRequirements.pipe(
|
|
||||||
filter(deps => !!deps),
|
|
||||||
switchMap(deps => this.appModel.watchForInstallations(deps)),
|
|
||||||
concatMap(() => markAsLoadingDuring$(this.$dependenciesLoading$, this.syncVersionSpecificInfo())),
|
|
||||||
catchError(e => of(console.error(e))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private setError (e: Error) {
|
|
||||||
console.error(e)
|
|
||||||
this.$error$.next(e.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import { AppConfigObjectPageModule } from 'src/app/modals/app-config-object/app-
|
|||||||
import { AppConfigUnionPageModule } from 'src/app/modals/app-config-union/app-config-union.module'
|
import { 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 { AppConfigValuePageModule } from 'src/app/modals/app-config-value/app-config-value.module'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
|
||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
|
||||||
import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module'
|
import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module'
|
||||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||||
|
|
||||||
@@ -19,7 +17,6 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: AppConfigPage,
|
component: AppConfigPage,
|
||||||
// canDeactivate: [CanDeactivateGuard],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -35,8 +32,6 @@ const routes: Routes = [
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
PwaBackComponentModule,
|
|
||||||
BadgeMenuComponentModule,
|
|
||||||
RecommendationButtonComponentModule,
|
RecommendationButtonComponentModule,
|
||||||
InformationPopoverComponentModule,
|
InformationPopoverComponentModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,22 +5,24 @@
|
|||||||
<ion-icon name="arrow-back"></ion-icon>
|
<ion-icon name="arrow-back"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>{{ app['title'] | async }}</ion-title>
|
<ion-title>{{ pkg.manifest.title }}</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
|
|
||||||
<!-- loading -->
|
<!-- loading -->
|
||||||
<div *ngIf="$loading$ | async" class="full-page-spinner">
|
<ion-grid *ngIf="loadingText$ | ngrxPush as loadingText; else loaded" style="height: 100%;">
|
||||||
<ion-spinner style="justify-self: center; align-self: end;" name="lines" color="warning"></ion-spinner>
|
<ion-row class="ion-align-items-center ion-text-center" style="height: 100%;">
|
||||||
<ion-label style="justify-self: center;" *ngIf="($loadingText$ | async)" color="dark">
|
<ion-col>
|
||||||
{{$loadingText$ | async}}
|
<ion-spinner name="lines" color="warning"></ion-spinner>
|
||||||
</ion-label>
|
<p>{{ loadingText }}</p>
|
||||||
</div>
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
<!-- not loading -->
|
<!-- not loading -->
|
||||||
<ng-container *ngIf="!($loading$ | async)">
|
<ng-template #loaded>
|
||||||
<ion-item *ngIf="error" class="notifier-item">
|
<ion-item *ngIf="error" class="notifier-item">
|
||||||
<ion-label style="margin: 7px 5px;" class="ion-text-wrap">
|
<ion-label style="margin: 7px 5px;" class="ion-text-wrap">
|
||||||
<p style="color: var(--ion-color-danger)">{{ error.text }}</p>
|
<p style="color: var(--ion-color-danger)">{{ error.text }}</p>
|
||||||
@@ -33,44 +35,45 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button style="position: absolute; right: 0; top: 0" *ngIf="app && (app.id | async)" color="danger" fill="clear" (click)="dismissError()">
|
<ion-button style="position: absolute; right: 0; top: 0" *ngIf="pkg" color="danger" fill="clear" (click)="dismissError()">
|
||||||
<ion-icon name="close-outline"></ion-icon>
|
<ion-icon name="close-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-container *ngIf="app && (app.id | async)">
|
<ng-container *ngIf="pkg">
|
||||||
<ng-container *ngIf="([AppStatus.NEEDS_CONFIG] | includes: (app.status | async)) && !edited">
|
<!-- @TODO make sure this is how to determine if pkg is in needs_config -->
|
||||||
|
<ng-container *ngIf="pkg.manifest.config && !pkg.status.configured && !edited">
|
||||||
<ion-item class="notifier-item">
|
<ion-item class="notifier-item">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<h2 style="display: flex; align-items: center; margin-bottom: 3px;">
|
<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-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>
|
<ion-text style="font-size: smaller;">Initial Config</ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
<p style="font-size: small">To use the default config for {{ app.title | async }}, click "Save" below.</p>
|
<p style="font-size: small">To use the default config for {{ app.title | ngrxPush }}, click "Save" below.</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="recommendation && showRecommendation">
|
<ng-container *ngIf="rec && showRec">
|
||||||
<ion-item class="recommendation-item">
|
<ion-item class="rec-item">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<h2 style="display: flex; align-items: center;">
|
<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-icon size="small" style="margin: 4px" slot="start" color="primary" slot="start" name="ellipse"></ion-icon>
|
||||||
<ion-avatar style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
<ion-avatar style="width: 3vh; height: 3vh; margin: 0px 2px 0px 5px;" slot="start">
|
||||||
<img [src]="recommendation.iconURL | iconParse" [alt]="recommendation.title"/>
|
<img [src]="rec.dependentIcon" [alt]="rec.dependentTitle"/>
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{recommendation.title}}</ion-text>
|
<ion-text style="margin: 5px; font-family: 'Montserrat'; font-size: smaller;">{{ rec.dependentTitle }}</ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
<div style="margin: 7px 5px;">
|
<div style="margin: 7px 5px;">
|
||||||
<p style="font-size: small; color: var(--ion-color-medium)"> {{app.title | async}} config has been modified to satisfy {{recommendation.title}}.
|
<p style="font-size: small; color: var(--ion-color-medium)"> {{app.title | ngrxPush}} config has been modified to satisfy {{ rec.dependentTitle }}.
|
||||||
<ion-text color="dark">To accept the changes, click “Save” below.</ion-text>
|
<ion-text color="dark">To accept the changes, click “Save” below.</ion-text>
|
||||||
</p>
|
</p>
|
||||||
<a style="font-size: small" *ngIf="!openRecommendation" (click)="openRecommendation = true">More Info</a>
|
<a style="font-size: small" *ngIf="!openRec" (click)="openRec = true">More Info</a>
|
||||||
<ng-container *ngIf="openRecommendation">
|
<ng-container *ngIf="openRec">
|
||||||
<p style="margin-top: 10px; color: var(--ion-color-medium); font-size: small" [innerHTML]="recommendation.description"></p>
|
<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)="openRecommendation = false">hide</a>
|
<a style="font-size: x-small; font-style: italic;" (click)="openRec = false">hide</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRecommendation()">
|
<ion-button style="position: absolute; right: 0; top: 0" color="primary" fill="clear" (click)="dismissRec()">
|
||||||
<ion-icon name="close-outline"></ion-icon>
|
<ion-icon name="close-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,18 +92,18 @@
|
|||||||
<!-- no config -->
|
<!-- no config -->
|
||||||
<ion-item *ngIf="!hasConfig">
|
<ion-item *ngIf="!hasConfig">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<p>No config options for {{ app.title | async }} {{ app.versionInstalled | async }}.</p>
|
<p>No config options for {{ app.title | ngrxPush }} {{ app.versionInstalled | ngrxPush }}.</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<!-- save button, always show -->
|
<!-- save button, always show -->
|
||||||
<ion-button
|
<ion-button
|
||||||
[disabled]="invalid || (!edited && !added && !(['NEEDS_CONFIG'] | includes: (app.status | async)))"
|
[disabled]="invalid || (!edited && !added && !(['NEEDS_CONFIG'] | includes: (app.status | ngrxPush)))"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
expand="block"
|
expand="block"
|
||||||
style="margin: 10px"
|
style="margin: 10px"
|
||||||
color="primary"
|
color="primary"
|
||||||
(click)="save()"
|
(click)="save(pkg)"
|
||||||
>
|
>
|
||||||
<ion-text color="primary" style="font-weight: bold">
|
<ion-text color="primary" style="font-weight: bold">
|
||||||
Save
|
Save
|
||||||
@@ -115,5 +118,5 @@
|
|||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,80 +1,73 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { NavController, AlertController, ModalController, PopoverController } from '@ionic/angular'
|
import { NavController, AlertController, ModalController, PopoverController } from '@ionic/angular'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
|
||||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { pauseFor, isEmptyObject, modulateTime } from 'src/app/util/misc.util'
|
import { isEmptyObject } from 'src/app/util/misc.util'
|
||||||
import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util'
|
import { LoaderService } from 'src/app/services/loader.service'
|
||||||
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
|
||||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||||
import { ModelPreload } from 'src/app/models/model-preload'
|
import { BehaviorSubject, from, fromEvent, of, Subscription } from 'rxjs'
|
||||||
import { BehaviorSubject, forkJoin, from, fromEvent, of } from 'rxjs'
|
|
||||||
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
|
import { catchError, concatMap, map, take, tap } from 'rxjs/operators'
|
||||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||||
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 { 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 { ConfigSpec } from 'src/app/app-config/config-types'
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||||
|
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||||
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-config',
|
selector: 'app-config',
|
||||||
templateUrl: './app-config.page.html',
|
templateUrl: './app-config.page.html',
|
||||||
styleUrls: ['./app-config.page.scss'],
|
styleUrls: ['./app-config.page.scss'],
|
||||||
})
|
})
|
||||||
export class AppConfigPage extends Cleanup {
|
export class AppConfigPage {
|
||||||
error: { text: string, moreInfo?:
|
error: { text: string, moreInfo?:
|
||||||
{ title: string, description: string, buttonText: string }
|
{ title: string, description: string, buttonText: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid: string
|
loadingText$ = new BehaviorSubject(undefined)
|
||||||
$loading$ = new BehaviorSubject(true)
|
|
||||||
$loadingText$ = new BehaviorSubject(undefined)
|
|
||||||
|
|
||||||
app: PropertySubject<AppInstalledFull> = { } as any
|
pkg: InstalledPackageDataEntry
|
||||||
appId: string
|
|
||||||
hasConfig = false
|
hasConfig = false
|
||||||
|
|
||||||
recommendation: Recommendation | null = null
|
backButtonDefense = false
|
||||||
showRecommendation = true
|
|
||||||
openRecommendation = false
|
|
||||||
|
|
||||||
|
rec: Recommendation | null = null
|
||||||
|
showRec = true
|
||||||
|
openRec = false
|
||||||
|
|
||||||
|
invalid: string
|
||||||
edited: boolean
|
edited: boolean
|
||||||
added: boolean
|
added: boolean
|
||||||
rootCursor: ConfigCursor<'object'>
|
rootCursor: ConfigCursor<'object'>
|
||||||
spec: ConfigSpec
|
spec: ConfigSpec
|
||||||
config: object
|
config: object
|
||||||
|
|
||||||
AppStatus = AppStatus
|
subs: Subscription[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly navCtrl: NavController,
|
private readonly navCtrl: NavController,
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly wizardBaker: WizardBaker,
|
private readonly wizardBaker: WizardBaker,
|
||||||
private readonly preload: ModelPreload,
|
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly loader: LoaderService,
|
private readonly loader: LoaderService,
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly modalController: ModalController,
|
private readonly modalController: ModalController,
|
||||||
private readonly trackingModalCtrl: TrackingModalController,
|
private readonly trackingModalCtrl: TrackingModalController,
|
||||||
private readonly popoverController: PopoverController,
|
private readonly popoverController: PopoverController,
|
||||||
private readonly appModel: AppModel,
|
private readonly patch: PatchDbModel,
|
||||||
) { super() }
|
) { }
|
||||||
|
|
||||||
backButtonDefense = false
|
|
||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
const pkgId = this.route.snapshot.paramMap.get('pkgId') as string
|
||||||
|
|
||||||
|
this.subs = [
|
||||||
this.route.params.pipe(take(1)).subscribe(params => {
|
this.route.params.pipe(take(1)).subscribe(params => {
|
||||||
if (params.edit) {
|
if (params.edit) {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
|
||||||
this.cleanup(
|
|
||||||
fromEvent(window, 'popstate').subscribe(() => {
|
fromEvent(window, 'popstate').subscribe(() => {
|
||||||
this.backButtonDefense = false
|
this.backButtonDefense = false
|
||||||
this.trackingModalCtrl.dismissAll()
|
this.trackingModalCtrl.dismissAll()
|
||||||
@@ -90,29 +83,28 @@ export class AppConfigPage extends Cleanup {
|
|||||||
this.navCtrl.back()
|
this.navCtrl.back()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
]
|
||||||
|
|
||||||
markAsLoadingDuring$(this.$loading$,
|
this.patch.watch$('package-data', pkgId, 'installed')
|
||||||
from(this.preload.appFull(this.appId))
|
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(app => this.app = app),
|
tap(pkg => this.pkg = pkg),
|
||||||
tap(() => this.$loadingText$.next(`Fetching config spec...`)),
|
tap(() => this.loadingText$.next(`Fetching config spec...`)),
|
||||||
concatMap(() => forkJoin([this.apiService.getAppConfig(this.appId), pauseFor(600)])),
|
concatMap(() => this.apiService.getPackageConfig({ id: pkgId })),
|
||||||
concatMap(([{ spec, config }]) => {
|
concatMap(({ spec, config }) => {
|
||||||
const rec = history.state && history.state.configRecommendation as Recommendation
|
const rec = history.state && history.state.configRecommendation as Recommendation
|
||||||
if (rec) {
|
if (rec) {
|
||||||
this.$loadingText$.next(`Setting properties to accomodate ${rec.title}...`)
|
this.loadingText$.next(`Setting properties to accommodate ${rec.dependentTitle}...`)
|
||||||
return from(this.apiService.postConfigureDependency(this.appId, rec.appId, true))
|
return from(this.apiService.dryConfigureDependency({ 'dependency-id': pkgId, 'dependent-id': rec.dependentId }))
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => ({
|
map(res => ({
|
||||||
spec,
|
spec,
|
||||||
config,
|
config,
|
||||||
dependencyConfig: res.config,
|
dependencyConfig: res,
|
||||||
})),
|
})),
|
||||||
tap(() => this.recommendation = rec),
|
tap(() => this.rec = rec),
|
||||||
catchError(e => {
|
catchError(e => {
|
||||||
this.error = { text: `Could not set properties to accomodate ${rec.title}: ${e.message}`, moreInfo: {
|
this.error = { text: `Could not set properties to accommodate ${rec.dependentTitle}: ${e.message}`, moreInfo: {
|
||||||
title: `${rec.title} requires the following:`,
|
title: `${rec.dependentTitle} requires the following:`,
|
||||||
description: rec.description,
|
description: rec.description,
|
||||||
buttonText: 'Configure Manually',
|
buttonText: 'Configure Manually',
|
||||||
} }
|
} }
|
||||||
@@ -124,15 +116,18 @@ export class AppConfigPage extends Cleanup {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
|
map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)),
|
||||||
tap(() => this.$loadingText$.next(undefined)),
|
tap(() => this.loadingText$.next(undefined)),
|
||||||
),
|
take(1),
|
||||||
).subscribe({
|
).subscribe({
|
||||||
error: e => {
|
error: e => {
|
||||||
console.error(e)
|
console.error(e.message)
|
||||||
this.error = { text: e.message }
|
this.error = { text: e.message }
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
)
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
this.subs.forEach(sub => sub.unsubscribe())
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentPopover (title: string, description: string, ev: any) {
|
async presentPopover (title: string, description: string, ev: any) {
|
||||||
@@ -165,8 +160,8 @@ export class AppConfigPage extends Cleanup {
|
|||||||
this.hasConfig = !isEmptyObject(this.spec)
|
this.hasConfig = !isEmptyObject(this.spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissRecommendation () {
|
dismissRec () {
|
||||||
this.showRecommendation = false
|
this.showRec = false
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissError () {
|
dismissError () {
|
||||||
@@ -181,38 +176,30 @@ export class AppConfigPage extends Cleanup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save () {
|
async save (pkg: InstalledPackageDataEntry) {
|
||||||
const app = peekProperties(this.app)
|
|
||||||
const ogAppStatus = app.status
|
|
||||||
|
|
||||||
return this.loader.of({
|
return this.loader.of({
|
||||||
message: `Saving config...`,
|
message: `Saving config...`,
|
||||||
spinner: 'lines',
|
spinner: 'lines',
|
||||||
cssClass: 'loader',
|
cssClass: 'loader',
|
||||||
}).displayDuringAsync(async () => {
|
}).displayDuringAsync(async () => {
|
||||||
const config = this.config
|
const { breakages } = await this.apiService.drySetPackageConfig({ id: pkg.manifest.id, config: this.config })
|
||||||
const { breakages } = await this.apiService.patchAppConfig(app, config, true)
|
|
||||||
|
|
||||||
if (breakages.length) {
|
if (breakages.length) {
|
||||||
const { cancelled } = await wizardModal(
|
const { cancelled } = await wizardModal(
|
||||||
this.modalController,
|
this.modalController,
|
||||||
this.wizardBaker.configure({
|
this.wizardBaker.configure({
|
||||||
app,
|
pkg,
|
||||||
breakages,
|
breakages,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (cancelled) return { skip: true }
|
if (cancelled) return { skip: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.apiService.patchAppConfig(app, config).then(
|
return this.apiService.setPackageConfig({ id: pkg.manifest.id, config: this.config })
|
||||||
() => this.preload.loadInstalledApp(this.appId).then(() => ({ skip: false })),
|
.then(() => ({ skip: false }))
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.then(({ skip }) => {
|
.then(({ skip }) => {
|
||||||
if (skip) return
|
if (skip) return
|
||||||
if (ogAppStatus === AppStatus.RUNNING) {
|
|
||||||
this.appModel.update({ id: this.appId, status: AppStatus.RESTARTING }, modulateTime(new Date(), 3, 'seconds'))
|
|
||||||
}
|
|
||||||
this.navCtrl.back()
|
this.navCtrl.back()
|
||||||
})
|
})
|
||||||
.catch(e => this.error = { text: e.message })
|
.catch(e => this.error = { text: e.message })
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
|
|
||||||
import { DependencyListComponentModule } from 'src/app/components/dependency-list/dependency-list.component.module'
|
|
||||||
import { AppInstalledListPage } from './app-installed-list.page'
|
import { AppInstalledListPage } from './app-installed-list.page'
|
||||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { AppBackupPageModule } from 'src/app/modals/app-backup/app-backup.module'
|
|
||||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
|
||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -23,14 +18,13 @@ const routes: Routes = [
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
StatusComponentModule,
|
StatusComponentModule,
|
||||||
DependencyListComponentModule,
|
|
||||||
AppBackupPageModule,
|
|
||||||
SharingModule,
|
SharingModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
PwaBackComponentModule,
|
|
||||||
BadgeMenuComponentModule,
|
BadgeMenuComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [AppInstalledListPage],
|
declarations: [
|
||||||
|
AppInstalledListPage,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppInstalledListPageModule { }
|
export class AppInstalledListPageModule { }
|
||||||
|
|||||||
@@ -8,52 +8,9 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content style="position: relative">
|
<ion-content style="position: relative">
|
||||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
<ng-container *ngrxLet="patch.watch$('package-data') as pkgs">
|
||||||
|
|
||||||
<ng-container *ngIf="!($loading$ | async)">
|
<div *ngIf="pkgs | empty; else list" class="ion-text-center ion-padding">
|
||||||
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
|
|
||||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
|
||||||
</ion-refresher>
|
|
||||||
|
|
||||||
<ion-item *ngIf="error">
|
|
||||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-grid>
|
|
||||||
<ion-row>
|
|
||||||
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
|
|
||||||
<ng-container *ngIf="{
|
|
||||||
status: app.subject.status | async,
|
|
||||||
hasUI: app.subject.hasUI | async,
|
|
||||||
launchable: app.subject.launchable | async,
|
|
||||||
iconURL: app.subject.iconURL | async | iconParse,
|
|
||||||
title: app.subject.title | async
|
|
||||||
} as vars">
|
|
||||||
|
|
||||||
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
|
|
||||||
<div class="launch-container" *ngIf="vars.hasUI">
|
|
||||||
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="!vars.launchable">
|
|
||||||
<ion-icon name="rocket-outline"></ion-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="vars.title" />
|
|
||||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
|
||||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
|
|
||||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
|
|
||||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
|
|
||||||
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
|
|
||||||
<ion-card-header>
|
|
||||||
<status [appStatus]="vars.status" size="small"></status>
|
|
||||||
<p>{{ vars.title }}</p>
|
|
||||||
</ion-card-header>
|
|
||||||
</ion-card>
|
|
||||||
</ng-container>
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
</ion-grid>
|
|
||||||
|
|
||||||
<div *ngIf="!apps || !apps.length" class="ion-text-center ion-padding">
|
|
||||||
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
|
<div style="display: flex; flex-direction: column; justify-content: center; height: 40vh">
|
||||||
<h2>Welcome to your <span style="font-style: italic; color: var(--ion-color-start9)">Embassy</span></h2>
|
<h2>Welcome to your <span style="font-style: italic; color: var(--ion-color-start9)">Embassy</span></h2>
|
||||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||||
@@ -63,5 +20,32 @@
|
|||||||
Marketplace
|
Marketplace
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #list>
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row *ngrxLet="connectionService.monitor$() as connection">
|
||||||
|
<ion-col *ngFor="let pkg of pkgs | keyvalue : asIsOrder" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
|
||||||
|
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', (pkg.value | manifest).id]">
|
||||||
|
<div class="launch-container" *ngIf="pkg.value | hasUi">
|
||||||
|
<div class="launch-button-triangle" (click)="launchUi(pkg.value, $event)" [class.disabled]="!(pkg.value | isLaunchable)">
|
||||||
|
<ion-icon name="rocket-outline"></ion-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img style="position: absolute" class="main-img" [src]="pkg.value['static-files'].icon" [alt]="icon" />
|
||||||
|
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||||
|
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'green' : connection" src="assets/img/running-bulb.png"/>
|
||||||
|
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'red' : connection" src="assets/img/issue-bulb.png"/>
|
||||||
|
<img class="bulb-on" *ngIf="pkg.value | displayBulb: 'yellow' : connection" src="assets/img/warning-bulb.png"/>
|
||||||
|
<img class="bulb-off" *ngIf="pkg.value | displayBulb: 'off' : connection" src="assets/img/off-bulb.png"/>
|
||||||
|
<ion-card-header>
|
||||||
|
<status [pkg]="pkg.value" [connection]="connection" size="small"></status>
|
||||||
|
<p>{{ (pkg.value | manifest).title }}</p>
|
||||||
|
</ion-card-header>
|
||||||
|
</ion-card>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,126 +1,29 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
|
||||||
import { AppInstalledPreview } from 'src/app/models/app-types'
|
|
||||||
import { ModelPreload } from 'src/app/models/model-preload'
|
|
||||||
import { doForAtLeast } from 'src/app/util/misc.util'
|
|
||||||
import { PropertySubject, PropertySubjectId, toObservable } from 'src/app/util/property-subject.util'
|
|
||||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs'
|
|
||||||
import { S9Server, ServerModel, ServerStatus } from 'src/app/models/server-model'
|
|
||||||
import { SyncDaemon } from 'src/app/services/sync.service'
|
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
|
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-installed-list',
|
selector: 'app-installed-list',
|
||||||
templateUrl: './app-installed-list.page.html',
|
templateUrl: './app-installed-list.page.html',
|
||||||
styleUrls: ['./app-installed-list.page.scss'],
|
styleUrls: ['./app-installed-list.page.scss'],
|
||||||
})
|
})
|
||||||
export class AppInstalledListPage extends Cleanup {
|
export class AppInstalledListPage {
|
||||||
error = ''
|
|
||||||
initError = ''
|
|
||||||
$loading$ = new BehaviorSubject(true)
|
|
||||||
s9Host$: Observable<string>
|
|
||||||
|
|
||||||
AppStatus = AppStatus
|
|
||||||
|
|
||||||
server: PropertySubject<S9Server>
|
|
||||||
currentServer: S9Server
|
|
||||||
apps: PropertySubjectId<AppInstalledPreview>[] = []
|
|
||||||
|
|
||||||
subsToTearDown: Subscription[] = []
|
|
||||||
|
|
||||||
updatingFreeze = false
|
|
||||||
updating = false
|
|
||||||
segmentValue: 'services' | 'embassy' = 'services'
|
|
||||||
|
|
||||||
showCertDownload : boolean
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly serverModel: ServerModel,
|
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly preload: ModelPreload,
|
|
||||||
private readonly syncDaemon: SyncDaemon,
|
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) {
|
public readonly connectionService: ConnectionService,
|
||||||
super()
|
public readonly patch: PatchDbModel,
|
||||||
}
|
) { }
|
||||||
|
|
||||||
ngOnDestroy () {
|
launchUi (pkg: PackageDataEntry, event: Event): void {
|
||||||
this.subsToTearDown.forEach(s => s.unsubscribe())
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit () {
|
|
||||||
this.server = this.serverModel.watch()
|
|
||||||
this.apps = []
|
|
||||||
this.cleanup(
|
|
||||||
|
|
||||||
// serverUpdateSubscription
|
|
||||||
this.server.status.subscribe(status => {
|
|
||||||
if (status === ServerStatus.UPDATING) {
|
|
||||||
this.updating = true
|
|
||||||
} else {
|
|
||||||
if (!this.updatingFreeze) { this.updating = false }
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
// newAppsSubscription
|
|
||||||
this.appModel.watchDelta('add').subscribe(({ id }) => {
|
|
||||||
if (this.apps.find(a => a.id === id)) return
|
|
||||||
this.apps.push({ id, subject: this.appModel.watch(id) })
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// appsDeletedSubscription
|
|
||||||
this.appModel.watchDelta('delete').subscribe(({ id }) => {
|
|
||||||
const i = this.apps.findIndex(a => a.id === id)
|
|
||||||
this.apps.splice(i, 1)
|
|
||||||
}),
|
|
||||||
|
|
||||||
// currentServerSubscription
|
|
||||||
toObservable(this.server).subscribe(currentServerProperties => {
|
|
||||||
this.currentServer = currentServerProperties
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
markAsLoadingDuring$(this.$loading$, this.preload.apps()).subscribe({
|
|
||||||
next: apps => {
|
|
||||||
this.apps = apps
|
|
||||||
},
|
|
||||||
error: e => {
|
|
||||||
console.error(e)
|
|
||||||
this.error = e.message
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async launchUiTab (id: string, event: Event) {
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
window.open(this.config.launchableURL(pkg.installed), '_blank')
|
||||||
const app = this.apps.find(app => app.id === id).subject
|
|
||||||
|
|
||||||
let uiAddress: string
|
|
||||||
if (this.config.isTor()) {
|
|
||||||
uiAddress = `http://${app.torAddress.getValue()}`
|
|
||||||
} else {
|
|
||||||
uiAddress = `https://${app.lanAddress.getValue()}`
|
|
||||||
}
|
|
||||||
return window.open(uiAddress, '_blank')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh (event: any) {
|
asIsOrder () {
|
||||||
await doForAtLeast([this.getServerAndApps()], 600)
|
return 0
|
||||||
event.target.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getServerAndApps (): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.syncDaemon.sync()
|
|
||||||
this.error = ''
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
this.error = e.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
|
|
||||||
import { DependencyListComponentModule } from 'src/app/components/dependency-list/dependency-list.component.module'
|
|
||||||
import { AppInstalledShowPage } from './app-installed-show.page'
|
import { AppInstalledShowPage } from './app-installed-show.page'
|
||||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||||
import { AppBackupPageModule } from 'src/app/modals/app-backup/app-backup.module'
|
|
||||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
import { 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 { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||||
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
|
|
||||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -26,15 +21,12 @@ const routes: Routes = [
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
StatusComponentModule,
|
StatusComponentModule,
|
||||||
DependencyListComponentModule,
|
|
||||||
AppBackupPageModule,
|
|
||||||
SharingModule,
|
SharingModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
PwaBackComponentModule,
|
PwaBackComponentModule,
|
||||||
BadgeMenuComponentModule,
|
BadgeMenuComponentModule,
|
||||||
InstallWizardComponentModule,
|
InstallWizardComponentModule,
|
||||||
ErrorMessageComponentModule,
|
|
||||||
InformationPopoverComponentModule,
|
InformationPopoverComponentModule,
|
||||||
],
|
],
|
||||||
declarations: [AppInstalledShowPage],
|
declarations: [AppInstalledShowPage],
|
||||||
|
|||||||
@@ -10,31 +10,16 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content *ngIf="{
|
<ion-content class="ion-padding-bottom">
|
||||||
id: app.id | async,
|
|
||||||
torAddress: app.torAddress | async,
|
|
||||||
status: app.status | async,
|
|
||||||
versionInstalled: app.versionInstalled | async,
|
|
||||||
licenseName: app.licenseName | async,
|
|
||||||
licenseLink: app.licenseLink | async,
|
|
||||||
configuredRequirements: app.configuredRequirements | async,
|
|
||||||
lastBackup: app.lastBackup | async,
|
|
||||||
hasFetchedFull: app.hasFetchedFull | async,
|
|
||||||
iconURL: app.iconURL | async,
|
|
||||||
title: app.title | async,
|
|
||||||
hasUI: app.hasUI | async,
|
|
||||||
launchable: app.launchable | async,
|
|
||||||
lanAddress: app.lanAddress | async
|
|
||||||
} as vars" class="ion-padding-bottom">
|
|
||||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
|
||||||
<ng-container *ngIf="!($loading$ | async)">
|
|
||||||
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
|
|
||||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
|
||||||
</ion-refresher>
|
|
||||||
|
|
||||||
<error-message [$error$]="$error$" [dismissable]="!!(app && app.id)"></error-message>
|
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||||
|
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
<div class="top-plate" *ngIf="app && app.id">
|
<ng-container *ngrxLet="connectionService.monitor$() as connection">
|
||||||
|
<ng-container *ngIf="pkg | manifest as manifest">
|
||||||
|
<ng-container *ngIf="pkg | status : connection as status">
|
||||||
|
<div class="top-plate">
|
||||||
<ion-item class="no-cushion-item" lines="none">
|
<ion-item class="no-cushion-item" lines="none">
|
||||||
<ion-label class="ion-text-wrap" style="
|
<ion-label class="ion-text-wrap" style="
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -43,14 +28,14 @@
|
|||||||
margin-top: 15px;"
|
margin-top: 15px;"
|
||||||
>
|
>
|
||||||
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
|
<ion-avatar style="justify-self: center; height: 55px; width: 55px" slot="start">
|
||||||
<img [src]="vars.iconURL | iconParse" />
|
<img [src]="pkg['static-files'].icon" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<div style="display: flex; flex-direction: column;">
|
<div style="display: flex; flex-direction: column;">
|
||||||
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="vars.title.length > 20">
|
<ion-text style="font-family: 'Montserrat'; font-size: x-large; line-height: normal;" [class.less-large]="manifest.title.length > 20">
|
||||||
{{ vars.title }}
|
{{ manifest.title }}
|
||||||
</ion-text>
|
</ion-text>
|
||||||
<ion-text style="margin-top: -5px; margin-left: 2px;">
|
<ion-text style="margin-top: -5px; margin-left: 2px;">
|
||||||
{{ vars.versionInstalled | displayEmver }}
|
{{ manifest.version | displayEmver }}
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</div>
|
</div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
@@ -58,89 +43,48 @@
|
|||||||
|
|
||||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
|
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
|
||||||
<ion-label class="status-readout">
|
<ion-label class="status-readout">
|
||||||
<status size="bold-large" [appStatus]="vars.status"></status>
|
<status size="bold-large" [pkg]="pkg" [connection]="connection"></status>
|
||||||
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
|
<ion-button *ngIf="status === FeStatus.NeedsConfig" expand="block" fill="outline" [routerLink]="['config']">
|
||||||
Configure
|
Configure
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" color="danger" (click)="stop()">
|
<ion-button *ngIf="status === FeStatus.Running" expand="block" fill="outline" color="danger" (click)="stop()">
|
||||||
Stop
|
Stop
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
|
<ion-button *ngIf="status === FeStatus.DependencyIssue" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||||
Stop Backup
|
|
||||||
</ion-button>
|
|
||||||
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
|
|
||||||
Force Uninstall
|
|
||||||
</ion-button>
|
|
||||||
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
|
|
||||||
Fix
|
Fix
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" color="success" (click)="tryStart()">
|
<ion-button *ngIf="status === FeStatus.Stopped" expand="block" fill="outline" color="success" (click)="tryStart()">
|
||||||
Start
|
Start
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-button size="small" *ngIf="vars.hasUI" [disabled]="!vars.launchable" class="launch-button" expand="block" (click)="launchUiTab()">
|
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
|
||||||
Launch Web Interface
|
Launch Web Interface
|
||||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="app && app.id && vars.status !== 'INSTALLING'">
|
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : status)">
|
||||||
<ion-item-group class="ion-padding-bottom">
|
<ion-item-group class="ion-padding-bottom">
|
||||||
<!-- addresses -->
|
<!-- interfaces -->
|
||||||
<ion-item>
|
<ion-item [routerLink]="['interfaces']">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-icon slot="start" name="aperture-outline" color="primary"></ion-icon>
|
||||||
<h2>Tor Address</h2>
|
<ion-label><ion-text color="primary">Interfaces</ion-text></ion-label>
|
||||||
<p>{{ vars.torAddress }}</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
|
||||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item lines="none">
|
|
||||||
<ion-label class="ion-text-wrap">
|
|
||||||
<h2>LAN Address</h2>
|
|
||||||
<p *ngIf="!hideLAN">{{ vars.lanAddress }}</p>
|
|
||||||
<p *ngIf="hideLAN"><ion-text color="warning">No LAN address for {{ vars.title }} {{ vars.versionInstalled }}</ion-text></p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-button *ngIf="!hideLAN" slot="end" fill="clear" (click)="copyLAN()">
|
|
||||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- backups -->
|
|
||||||
<ion-item-divider></ion-item-divider>
|
|
||||||
<!-- create backup -->
|
|
||||||
<ion-item button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('create')">
|
|
||||||
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
|
|
||||||
<ion-label style="display: flex; flex-direction: column;">
|
|
||||||
<ion-text color="primary">Create Backup</ion-text>
|
|
||||||
<ion-text color="medium" style="font-size: x-small">
|
|
||||||
Last Backup: {{vars.lastBackup ? (vars.lastBackup | date: 'short') : 'never'}}
|
|
||||||
</ion-text>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<!-- restore backup -->
|
|
||||||
<ion-item lines="none" button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('restore')">
|
|
||||||
<ion-icon slot="start" name="color-wand-outline" color="primary"></ion-icon>
|
|
||||||
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item-divider></ion-item-divider>
|
|
||||||
<!-- instructions -->
|
<!-- instructions -->
|
||||||
<ion-item [routerLink]="['instructions']">
|
<ion-item [routerLink]="['instructions']">
|
||||||
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
|
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
|
||||||
<ion-label><ion-text color="primary">Instructions</ion-text></ion-label>
|
<ion-label><ion-text color="primary">Instructions</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- config -->
|
<!-- config -->
|
||||||
<ion-item [disabled]="[AppStatus.CREATING_BACKUP, AppStatus.RESTORING_BACKUP, AppStatus.INSTALLING, AppStatus.DEAD] | includes: vars.status" [routerLink]="['config']">
|
<ion-item [disabled]="[FeStatus.Installing, FeStatus.Updating, FeStatus.Removing, FeStatus.BackingUp, FeStatus.Restoring] | includes : status" [routerLink]="['config']">
|
||||||
<ion-icon slot="start" name="construct-outline" color="primary"></ion-icon>
|
<ion-icon slot="start" name="construct-outline" color="primary"></ion-icon>
|
||||||
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
|
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- metrics -->
|
<!-- properties -->
|
||||||
<ion-item [routerLink]="['metrics']">
|
<ion-item [routerLink]="['properties']">
|
||||||
<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>
|
||||||
@@ -150,42 +94,89 @@
|
|||||||
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
|
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- logs -->
|
<!-- logs -->
|
||||||
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
|
<ion-item [routerLink]="['logs']">
|
||||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||||
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
|
<ion-label><ion-text color="primary">Logs</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<!-- restore -->
|
||||||
|
<ion-item button [disabled]="[FeStatus.Connecting, FeStatus.Installing, FeStatus.Updating, FeStatus.Stopping, FeStatus.Removing, FeStatus.BackingUp, FeStatus.Restoring] | includes : status" [routerLink]="['restore']">
|
||||||
|
<ion-icon slot="start" name="color-wand-outline" color="primary"></ion-icon>
|
||||||
|
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<!-- donate -->
|
||||||
|
<ion-item button [href]="manifest['donation-url']" target="_blank">
|
||||||
|
<ion-icon slot="start" name="shapes-outline" color="primary"></ion-icon>
|
||||||
|
<ion-label><ion-text color="primary">Donate</ion-text></ion-label>
|
||||||
|
</ion-item>
|
||||||
<!-- marketplace -->
|
<!-- marketplace -->
|
||||||
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
|
<ion-item [routerLink]="['/services', 'marketplace', manifest.id]">
|
||||||
<ion-icon slot="start" name="storefront-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>
|
||||||
<!-- license -->
|
|
||||||
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
|
||||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
|
||||||
<ion-label><ion-text color="primary">License</ion-text></ion-label>
|
|
||||||
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- dependencies -->
|
<!-- dependencies -->
|
||||||
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">
|
<ng-container *ngIf="!(manifest.dependencies | empty)">
|
||||||
<ion-item-divider [id]="'service-requirements-' + vars.id">Dependencies
|
<ion-item-divider id="dependencies">
|
||||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(dependencyDefintion(), $event)">
|
Dependencies
|
||||||
|
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(depDefinition, $event)">
|
||||||
<ion-icon name="help-circle-outline"></ion-icon>
|
<ion-icon name="help-circle-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<dependency-list [$loading$]="$loadingDependencies$" depType="installed" [hostApp]="app | peekProperties" [dependencies]="vars.configuredRequirements"></dependency-list>
|
|
||||||
|
<div *ngFor="let dep of pkg.installed['current-dependencies'] | keyvalue">
|
||||||
|
<ion-item *ngrxLet="patch.watch$('package-data', dep.key) as localDep" class="dependency-item" lines="none">
|
||||||
|
<ion-avatar slot="start" style="position: relative; height: 5vh; width: 5vh; margin: 0px;">
|
||||||
|
<div class="dep-badge" [class]="pkg.installed.status['dependency-errors'][dep.key] ? 'dep-issue' : 'dep-sat'"></div>
|
||||||
|
<img [src]="localDep ? localDep['static-files'].icon : pkg.installed.status['dependency-errors'][dep.key]?.icon" />
|
||||||
|
</ion-avatar>
|
||||||
|
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
|
||||||
|
<h4 style="font-family: 'Montserrat'">{{ localDep ? (localDep | manifest).title : pkg.installed.status['dependency-errors'][dep.key]?.title }}</h4>
|
||||||
|
<p style="font-size: small">{{ manifest.dependencies[dep.key].version | displayEmver }}</p>
|
||||||
|
<p style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="pkg.installed.status['dependency-errors'][dep.key] ? 'warning' : 'success'">{{ pkg.installed.status['dependency-errors'][dep.key] ? pkg.installed.status['dependency-errors'][dep.key].type : 'satisfied' }}</ion-text></p>
|
||||||
|
</ion-label>
|
||||||
|
|
||||||
|
<ion-button *ngIf="!pkg.installed.status['dependency-errors'][dep.key] || (pkg.installed.status['dependency-errors'][dep.key] && [DependencyErrorType.InterfaceHealthChecksFailed, DependencyErrorType.HealthChecksFailed] | includes : pkg.installed.status['dependency-errors'][dep.key].type)" slot="end" size="small" [routerLink]="['/services', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
|
||||||
|
View
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ng-container *ngIf="pkg.installed.status['dependency-errors'][dep.key]">
|
||||||
|
<ion-button *ngIf="!localDep" slot="end" size="small" (click)="fixDep('install', dep.key)" color="primary" fill="outline" style="font-size: x-small">
|
||||||
|
Install
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<ng-container *ngIf="localDep && localDep.state === PackageState.Installed">
|
||||||
|
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.NotRunning" slot="end" size="small" [routerLink]="['/services', 'installed', dep.key]" color="primary" fill="outline" style="font-size: x-small">
|
||||||
|
Start
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.IncorrectVersion" slot="end" size="small" (click)="fixDep('update', dep.key)" color="primary" fill="outline" style="font-size: x-small">
|
||||||
|
Update
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="pkg.installed.status['dependency-errors'][dep.key].type === DependencyErrorType.ConfigUnsatisfied" slot="end" size="small" (click)="fixDep('configure', dep.key)" color="primary" fill="outline" style="font-size: x-small">
|
||||||
|
Configure
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div *ngIf="localDep && localDep.state !== PackageState.Installed" slot="end" class="spinner">
|
||||||
|
<ion-spinner [color]="localDep.state === PackageState.Removing ? 'danger' : 'primary'" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
|
|
||||||
<ng-container *ngIf="vars.status !== AppStatus.INSTALLING && vars.status !== 'CREATING_BACKUP'">
|
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.BackingUp, FeStatus.Restoring] | includes : status)">
|
||||||
<!-- uninstall -->
|
<!-- uninstall -->
|
||||||
<ion-item style="--background: transparent" button (click)="uninstall()">
|
<ion-item button (click)="uninstall()">
|
||||||
<ion-icon slot="start" name="trash-outline" color="medium"></ion-icon>
|
<ion-icon slot="start" name="trash-outline" color="danger"></ion-icon>
|
||||||
<ion-label><ion-text color="danger">Uninstall</ion-text></ion-label>
|
<ion-label><ion-text color="danger">Uninstall</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -48,3 +48,19 @@
|
|||||||
--border-radius: 10px;
|
--border-radius: 10px;
|
||||||
margin: 12px 10px;
|
margin: 12px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dep-badge {
|
||||||
|
position: absolute; width: 2.5vh;
|
||||||
|
height: 2.5vh;
|
||||||
|
border-radius: 50px;
|
||||||
|
left: -1vh;
|
||||||
|
top: -1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dep-issue {
|
||||||
|
background: radial-gradient(var(--ion-color-warning) 40%, transparent)
|
||||||
|
}
|
||||||
|
|
||||||
|
.dep-sat {
|
||||||
|
background: radial-gradient(var(--ion-color-success) 40%, transparent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +1,37 @@
|
|||||||
import { Component, ViewChild } from '@angular/core'
|
import { Component, ViewChild } from '@angular/core'
|
||||||
import { AlertController, NavController, ToastController, ModalController, IonContent, PopoverController } from '@ionic/angular'
|
import { AlertController, NavController, ModalController, IonContent, PopoverController } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||||
import { copyToClipboard } from 'src/app/util/web.util'
|
import { chill } from 'src/app/util/misc.util'
|
||||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
import { LoaderService } from 'src/app/services/loader.service'
|
||||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
import { Observable, of, Subscription } from 'rxjs'
|
||||||
import { ModelPreload } from 'src/app/models/model-preload'
|
|
||||||
import { chill, pauseFor } from 'src/app/util/misc.util'
|
|
||||||
import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util'
|
|
||||||
import { AppBackupPage } from 'src/app/modals/app-backup/app-backup.page'
|
|
||||||
import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/app/services/loader.service'
|
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
|
||||||
import { 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, filter, switchMap, tap } from 'rxjs/operators'
|
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
|
||||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||||
|
import { DependencyErrorConfigUnsatisfied, DependencyErrorNotInstalled, DependencyErrorType, PackageDataEntry, PackageState } from 'src/app/models/patch-db/data-model'
|
||||||
|
import { FEStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-installed-show',
|
selector: 'app-installed-show',
|
||||||
templateUrl: './app-installed-show.page.html',
|
templateUrl: './app-installed-show.page.html',
|
||||||
styleUrls: ['./app-installed-show.page.scss'],
|
styleUrls: ['./app-installed-show.page.scss'],
|
||||||
})
|
})
|
||||||
export class AppInstalledShowPage extends Cleanup {
|
export class AppInstalledShowPage {
|
||||||
$loading$ = new BehaviorSubject(true)
|
error: string
|
||||||
$loadingDependencies$ = new BehaviorSubject(false) // when true, dependencies will render with spinners.
|
pkgId: string
|
||||||
|
pkg: PackageDataEntry
|
||||||
$error$ = new BehaviorSubject<string>('')
|
pkgSub: Subscription
|
||||||
app: PropertySubject<AppInstalledFull> = { } as any
|
|
||||||
appId: string
|
|
||||||
AppStatus = AppStatus
|
|
||||||
showInstructions = false
|
|
||||||
|
|
||||||
hideLAN: boolean
|
hideLAN: boolean
|
||||||
|
|
||||||
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
|
FeStatus = FEStatus
|
||||||
|
PackageState = PackageState
|
||||||
|
DependencyErrorType = DependencyErrorType
|
||||||
|
|
||||||
|
depDefinition = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||||
|
|
||||||
@ViewChild(IonContent) content: IonContent
|
@ViewChild(IonContent) content: IonContent
|
||||||
|
|
||||||
@@ -44,115 +40,44 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly navCtrl: NavController,
|
private readonly navCtrl: NavController,
|
||||||
private readonly loader: LoaderService,
|
private readonly loader: LoaderService,
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly preload: ModelPreload,
|
|
||||||
private readonly wizardBaker: WizardBaker,
|
private readonly wizardBaker: WizardBaker,
|
||||||
private readonly appModel: AppModel,
|
|
||||||
private readonly popoverController: PopoverController,
|
private readonly popoverController: PopoverController,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) {
|
public readonly patch: PatchDbModel,
|
||||||
super()
|
public readonly connectionService: ConnectionService,
|
||||||
}
|
) { }
|
||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
this.pkgId = this.route.snapshot.paramMap.get('pkgId')
|
||||||
|
this.pkgSub = this.patch.watch$('package-data', this.pkgId).subscribe(pkg => this.pkg = pkg)
|
||||||
this.cleanup(
|
|
||||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
|
|
||||||
.pipe(
|
|
||||||
tap(app => {
|
|
||||||
this.app = app
|
|
||||||
const appP = peekProperties(this.app)
|
|
||||||
this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0
|
|
||||||
}),
|
|
||||||
concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack
|
|
||||||
catchError(e => of(this.setError(e))),
|
|
||||||
).subscribe(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidEnter () {
|
async ngOnDestroy () {
|
||||||
markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())
|
this.pkgSub.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh (event: any) {
|
launchUiTab (): void {
|
||||||
await Promise.all([
|
window.open(this.config.launchableURL(this.pkg.installed), '_blank')
|
||||||
this.getApp(),
|
|
||||||
pauseFor(600),
|
|
||||||
])
|
|
||||||
event.target.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
async scrollToRequirements () {
|
|
||||||
return this.scrollToElement('service-requirements-' + this.appId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getApp (): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.preload.loadInstalledApp(this.appId)
|
|
||||||
this.clearError()
|
|
||||||
} catch (e) {
|
|
||||||
this.setError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async launchUiTab () {
|
|
||||||
let uiAddress: string
|
|
||||||
if (this.config.isTor()) {
|
|
||||||
uiAddress = `http://${this.app.torAddress.getValue()}`
|
|
||||||
} else {
|
|
||||||
uiAddress = `https://${this.app.lanAddress.getValue()}`
|
|
||||||
}
|
|
||||||
return window.open(uiAddress, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyTor () {
|
|
||||||
const app = peekProperties(this.app)
|
|
||||||
let message = ''
|
|
||||||
await copyToClipboard(app.torAddress || '').then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
|
||||||
|
|
||||||
const toast = await this.toastCtrl.create({
|
|
||||||
header: message,
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 1000,
|
|
||||||
cssClass: 'notification-toast',
|
|
||||||
})
|
|
||||||
await toast.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyLAN () {
|
|
||||||
const app = peekProperties(this.app)
|
|
||||||
let message = ''
|
|
||||||
await copyToClipboard(app.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
|
||||||
|
|
||||||
const toast = await this.toastCtrl.create({
|
|
||||||
header: message,
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 1000,
|
|
||||||
cssClass: 'notification-toast',
|
|
||||||
})
|
|
||||||
await toast.present()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop (): Promise<void> {
|
async stop (): Promise<void> {
|
||||||
const app = peekProperties(this.app)
|
const { id, title, version } = this.pkg.installed.manifest
|
||||||
|
|
||||||
await this.loader.of({
|
await this.loader.of({
|
||||||
message: `Stopping ${app.title}...`,
|
message: `Stopping...`,
|
||||||
spinner: 'lines',
|
spinner: 'lines',
|
||||||
cssClass: 'loader',
|
cssClass: 'loader',
|
||||||
}).displayDuringAsync(async () => {
|
}).displayDuringAsync(async () => {
|
||||||
const { breakages } = await this.apiService.stopApp(this.appId, true)
|
const { breakages } = await this.apiService.dryStopPackage({ id })
|
||||||
|
|
||||||
if (breakages.length) {
|
if (breakages.length) {
|
||||||
const { cancelled } = await wizardModal(
|
const { cancelled } = await wizardModal(
|
||||||
this.modalCtrl,
|
this.modalCtrl,
|
||||||
this.wizardBaker.stop({
|
this.wizardBaker.stop({
|
||||||
id: app.id,
|
id,
|
||||||
title: app.title,
|
title,
|
||||||
version: app.versionInstalled,
|
version,
|
||||||
breakages,
|
breakages,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -160,76 +85,28 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
if (cancelled) return { }
|
if (cancelled) return { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.apiService.stopApp(this.appId).then(chill)
|
return this.apiService.stopPackage({ id }).then(chill)
|
||||||
}).catch(e => this.setError(e))
|
}).catch(e => this.setError(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryStart (): Promise<void> {
|
async tryStart (): Promise<void> {
|
||||||
const app = peekProperties(this.app)
|
const message = this.pkg.installed.manifest.alerts.start
|
||||||
if (app.startAlert) {
|
if (message) {
|
||||||
this.presentAlertStart(app)
|
this.presentAlertStart(message)
|
||||||
} else {
|
} else {
|
||||||
this.start(app)
|
this.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentModalBackup (type: 'create' | 'restore') {
|
|
||||||
const modal = await this.modalCtrl.create({
|
|
||||||
backdropDismiss: false,
|
|
||||||
component: AppBackupPage,
|
|
||||||
presentingElement: await this.modalCtrl.getTop(),
|
|
||||||
componentProps: {
|
|
||||||
app: peekProperties(this.app),
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await modal.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentAlertStopBackup (): Promise<void> {
|
|
||||||
const app = peekProperties(this.app)
|
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
backdropDismiss: false,
|
|
||||||
header: 'Warning',
|
|
||||||
message: `${app.title} is not finished backing up. Are you sure you want stop the process?`,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Cancel',
|
|
||||||
role: 'cancel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Stop',
|
|
||||||
cssClass: 'alert-danger',
|
|
||||||
handler: () => {
|
|
||||||
this.stopBackup()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopBackup (): Promise<void> {
|
|
||||||
await this.loader.of({
|
|
||||||
message: `Stopping backup...`,
|
|
||||||
spinner: 'lines',
|
|
||||||
cssClass: 'loader',
|
|
||||||
}).displayDuringP(this.apiService.stopAppBackup(this.appId))
|
|
||||||
.catch (e => this.setError(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
async uninstall () {
|
async uninstall () {
|
||||||
const app = peekProperties(this.app)
|
const { id, title, version, alerts } = this.pkg.installed.manifest
|
||||||
|
|
||||||
const data = await wizardModal(
|
const data = await wizardModal(
|
||||||
this.modalCtrl,
|
this.modalCtrl,
|
||||||
this.wizardBaker.uninstall({
|
this.wizardBaker.uninstall({
|
||||||
id: app.id,
|
id,
|
||||||
title: app.title,
|
title,
|
||||||
version: app.versionInstalled,
|
version,
|
||||||
uninstallAlert: app.uninstallAlert,
|
uninstallAlert: alerts.uninstall,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -251,10 +128,64 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
return await popover.present()
|
return await popover.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async presentAlertStart (app: AppInstalledFull): Promise<void> {
|
scrollToRequirements () {
|
||||||
|
const el = document.getElementById('dependencies')
|
||||||
|
if (!el) return
|
||||||
|
let y = el.offsetTop
|
||||||
|
return this.content.scrollToPoint(0, y, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fixDep (action: 'install' | 'update' | 'configure', id: string): Promise<void> {
|
||||||
|
switch (action) {
|
||||||
|
case 'install':
|
||||||
|
case 'update':
|
||||||
|
return this.installDep(id)
|
||||||
|
case 'configure':
|
||||||
|
return this.configureDep(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async installDep (depId: string): Promise<void> {
|
||||||
|
const version = this.pkg.installed.manifest.dependencies[depId].version
|
||||||
|
const dependentTitle = this.pkg.installed.manifest.title
|
||||||
|
|
||||||
|
const installRec: Recommendation = {
|
||||||
|
dependentId: this.pkgId,
|
||||||
|
dependentTitle,
|
||||||
|
dependentIcon: this.pkg['static-files'].icon,
|
||||||
|
version,
|
||||||
|
description: `${dependentTitle} requires an install of ${(this.pkg.installed.status['dependency-errors'][depId] as DependencyErrorNotInstalled)?.title} satisfying ${version}.`,
|
||||||
|
}
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
state: { installRec },
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navCtrl.navigateForward(`/services/marketplace/${depId}`, navigationExtras)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async configureDep (depId: string): Promise<void> {
|
||||||
|
const configErrors = (this.pkg.installed.status['dependency-errors'][depId] as DependencyErrorConfigUnsatisfied).errors
|
||||||
|
|
||||||
|
const description = `<ul>${configErrors.map(d => `<li>${d}</li>`).join('\n')}</ul>`
|
||||||
|
const dependentTitle = this.pkg.installed.manifest.title
|
||||||
|
|
||||||
|
const configRecommendation: Recommendation = {
|
||||||
|
dependentId: this.pkgId,
|
||||||
|
dependentTitle,
|
||||||
|
dependentIcon: this.pkg['static-files'].icon,
|
||||||
|
description,
|
||||||
|
}
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
state: { configRecommendation },
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navCtrl.navigateForward(`/services/installed/${depId}/config`, navigationExtras)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async presentAlertStart (message: string): Promise<void> {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Warning',
|
header: 'Warning',
|
||||||
message: app.startAlert,
|
message,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
@@ -263,7 +194,7 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
{
|
{
|
||||||
text: 'Start',
|
text: 'Start',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.start(app)
|
this.start()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -271,40 +202,18 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async start (app: AppInstalledFull): Promise<void> {
|
private async start (): Promise<void> {
|
||||||
this.loader.of({
|
this.loader.of({
|
||||||
message: `Starting ${app.title}...`,
|
message: `Starting...`,
|
||||||
spinner: 'lines',
|
spinner: 'lines',
|
||||||
cssClass: 'loader',
|
cssClass: 'loader',
|
||||||
}).displayDuringP(
|
}).displayDuringP(
|
||||||
this.apiService.startApp(this.appId),
|
this.apiService.startPackage({ id: this.pkgId }),
|
||||||
).catch(e => this.setError(e))
|
).catch(e => this.setError(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
private setError (e: Error): Observable<void> {
|
private setError (e: Error): Observable<void> {
|
||||||
this.$error$.next(e.message)
|
this.error = e.message
|
||||||
return of()
|
return of()
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearError () {
|
|
||||||
this.$error$.next('')
|
|
||||||
}
|
|
||||||
|
|
||||||
private async scrollToElement (elementId: string) {
|
|
||||||
const el = document.getElementById(elementId)
|
|
||||||
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
let y = el.offsetTop
|
|
||||||
return this.content.scrollToPoint(0, y, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncWhenDependencyInstalls (): Observable<void> {
|
|
||||||
return this.app.configuredRequirements.pipe(
|
|
||||||
filter(deps => !!deps),
|
|
||||||
switchMap(reqs => this.appModel.watchForInstallations(reqs)),
|
|
||||||
concatMap(() => markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())),
|
|
||||||
catchError(e => of(console.error(e))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const routes: Routes = [
|
|||||||
PwaBackComponentModule,
|
PwaBackComponentModule,
|
||||||
SharingModule,
|
SharingModule,
|
||||||
],
|
],
|
||||||
declarations: [AppInstructionsPage],
|
declarations: [
|
||||||
|
AppInstructionsPage,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppInstructionsPageModule { }
|
export class AppInstructionsPageModule { }
|
||||||
|
|||||||
@@ -7,22 +7,14 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding">
|
||||||
|
<ion-spinner *ngIf="loading; else loaded" class="center" name="lines" color="warning"></ion-spinner>
|
||||||
|
|
||||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
<ng-template #loaded>
|
||||||
|
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||||
<ng-container *ngIf="!($loading$ | async)">
|
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||||
|
|
||||||
<ion-item *ngIf="!app.instructions">
|
|
||||||
<ion-label class="ion-text-wrap">
|
|
||||||
<p>No instructions for {{ app.title }} {{ app.versionInstalled }}.</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<div style="
|
<div *ngIf="instructions" class="instuctions-padding" [innerHTML]="instructions | markdown"></div>
|
||||||
padding-left: var(--ion-padding,16px);
|
</ng-template>
|
||||||
padding-right: var(--ion-padding,16px);
|
|
||||||
padding-bottom: var(--ion-padding,16px);
|
|
||||||
" *ngIf="app.instructions" [innerHTML]="app.instructions | markdown"></div>
|
|
||||||
</ng-container>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user