mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 04:53:40 +00:00
0.2.5 initial commit
Makefile incomplete
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<div style="position: relative; margin-right: 1vh;">
|
||||
<ion-icon
|
||||
*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>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { BadgeMenuComponent } from './badge-menu.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BadgeMenuComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [BadgeMenuComponent],
|
||||
})
|
||||
export class BadgeMenuComponentModule { }
|
||||
@@ -0,0 +1,17 @@
|
||||
.ios-badge {
|
||||
background-color: var(--ion-color-start9);
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 62%;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.md-badge {
|
||||
background-color: var(--ion-color-start9);
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 56%;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ServerModel } from '../../models/server-model'
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { isPlatform } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'badge-menu-button',
|
||||
templateUrl: './badge-menu.component.html',
|
||||
styleUrls: ['./badge-menu.component.scss'],
|
||||
})
|
||||
|
||||
export class BadgeMenuComponent {
|
||||
badge$: Observable<boolean>
|
||||
menuFixedOpen$: Observable<boolean>
|
||||
isIos: boolean
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
) {
|
||||
this.menuFixedOpen$ = this.splitPane.$menuFixedOpenOnLeft$.asObservable()
|
||||
this.badge$ = this.serverModel.watch().badge.pipe(map(i => i > 0))
|
||||
this.isIos = isPlatform('ios')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<ion-item-group>
|
||||
<!-- error -->
|
||||
<ng-container *ngIf="error">
|
||||
<ion-item>
|
||||
<ion-icon slot="start" name="warning-outline" color="danger" size="small"></ion-icon>
|
||||
<ion-label><ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider *ngIf="spec.description || spec.changeWarning"></ion-item-divider>
|
||||
</ng-container>
|
||||
<!-- description -->
|
||||
<ion-item *ngIf="spec.description">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p><ion-text color="dark">Description</ion-text></p>
|
||||
<p>{{ spec.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- warning -->
|
||||
<ion-item *ngIf="spec.changeWarning">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p><ion-text color="warning">Warning!</ion-text></p>
|
||||
<p>{{ spec.changeWarning }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ConfigHeaderComponent } from './config-header.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ConfigHeaderComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [ConfigHeaderComponent],
|
||||
})
|
||||
export class ConfigHeaderComponentModule { }
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ValueSpec } from 'src/app/app-config/config-types'
|
||||
|
||||
@Component({
|
||||
selector: 'config-header',
|
||||
templateUrl: './config-header.component.html',
|
||||
styleUrls: ['./config-header.component.scss'],
|
||||
})
|
||||
export class ConfigHeaderComponent {
|
||||
@Input() spec: ValueSpec
|
||||
@Input() error: string
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
@@ -0,0 +1,28 @@
|
||||
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 { }
|
||||
@@ -0,0 +1,30 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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 { }
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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 { }
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<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>
|
||||
@@ -0,0 +1,20 @@
|
||||
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 { }
|
||||
@@ -0,0 +1,10 @@
|
||||
.error-message {
|
||||
--background: var(--ion-color-danger);
|
||||
margin: 12px;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.legacy-error-message {
|
||||
margin: 5px;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div style="padding: 15px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
background: var(--ion-color-light);
|
||||
border-radius: 10px;
|
||||
border-color: var(--ion-color-warning);
|
||||
color: white;
|
||||
font-size: small;
|
||||
"
|
||||
[innerHTML]="unsafeInformation">
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { InformationPopoverComponent } from './information-popover.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
InformationPopoverComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [InformationPopoverComponent],
|
||||
})
|
||||
export class InformationPopoverComponentModule { }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
|
||||
@Component({
|
||||
selector: 'app-information-popover',
|
||||
templateUrl: './information-popover.component.html',
|
||||
styleUrls: ['./information-popover.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class InformationPopoverComponent implements OnInit {
|
||||
@Input() title: string
|
||||
@Input() information: string
|
||||
unsafeInformation: SafeHtml
|
||||
constructor (private sanitizer: DomSanitizer) { }
|
||||
ngOnInit () {
|
||||
this.unsafeInformation = this.sanitizer.bypassSecurityTrustHtml(this.information)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<div *ngIf="!($loading$ | async) && !params.skipCompletionDialogue" 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;">
|
||||
{{successText}}
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="long-message">
|
||||
{{summary}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="$loading$ | async" class="center-spinner">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<ion-label class="long-message">{{label}}</ion-label>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { CompleteComponent } from './complete.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CompleteComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [CompleteComponent],
|
||||
})
|
||||
export class CompleteComponentModule { }
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { BehaviorSubject, from, Subject } from 'rxjs'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||
import { Colorable, Loadable } from '../loadable'
|
||||
import { WizardAction } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'complete',
|
||||
templateUrl: './complete.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
})
|
||||
export class CompleteComponent implements OnInit, Loadable, Colorable {
|
||||
@Input() params: {
|
||||
action: WizardAction
|
||||
verb: string //loader verb: '*stopping* ...'
|
||||
title: string
|
||||
executeAction: () => Promise<any>
|
||||
skipCompletionDialogue?: boolean
|
||||
}
|
||||
|
||||
@Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise<any>
|
||||
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$color$ = new BehaviorSubject('medium')
|
||||
$cancel$ = new Subject<void>()
|
||||
|
||||
label: string
|
||||
summary: string
|
||||
successText: string
|
||||
|
||||
load () {
|
||||
markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe(
|
||||
{ error: e => this.finished({ error: new Error(`${this.params.action} failed: ${e.message || e}`) }),
|
||||
complete: () => this.params.skipCompletionDialogue && this.finished( { final: true} ),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
constructor () { }
|
||||
ngOnInit () {
|
||||
switch (this.params.action) {
|
||||
case 'install':
|
||||
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.$color$.next('primary')
|
||||
this.successText = 'In Progress'
|
||||
break
|
||||
case 'downgrade':
|
||||
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.$color$.next('primary')
|
||||
this.successText = 'In Progress'
|
||||
break
|
||||
case 'update':
|
||||
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.$color$.next('primary')
|
||||
this.successText = 'In Progress'
|
||||
break
|
||||
case 'uninstall':
|
||||
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully uninstalled.`
|
||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||
this.$color$.next('success')
|
||||
this.successText = 'Success'
|
||||
break
|
||||
case 'stop':
|
||||
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully stopped.`
|
||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||
this.$color$.next('success')
|
||||
this.successText = 'Success'
|
||||
break
|
||||
case 'configure':
|
||||
this.summary = `New config for ${this.params.title} has been successfully saved.`
|
||||
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
|
||||
this.$color$.next('success')
|
||||
this.successText = 'Success'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<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>
|
||||
@@ -0,0 +1,22 @@
|
||||
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 { }
|
||||
@@ -0,0 +1,127 @@
|
||||
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 { Colorable, 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, Colorable {
|
||||
@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
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<div>
|
||||
<div *ngIf="!($loading$ | async)" class="slide-content">
|
||||
<div style="margin-top: 25px;">
|
||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label color="warning" style="font-size: xx-large; font-weight: bold;"
|
||||
*ngIf="hasDependentViolation">
|
||||
WARNING
|
||||
</ion-label>
|
||||
<ion-label color="success" style="font-size: x-large; font-weight: bold; text-transform: capitalize;"
|
||||
*ngIf="!hasDependentViolation">
|
||||
READY
|
||||
</ion-label>
|
||||
</div>
|
||||
|
||||
<div *ngIf="longMessage" class="long-message" >
|
||||
{{longMessage}}
|
||||
</div>
|
||||
<div style="margin: 25px 0px;">
|
||||
<div style="border-width: 0px 0px 1px 0px; font-size: unset; text-align: left; font-weight: bold; margin-left: 13px; border-style: solid; border-color: var(--ion-color-light-tint);"
|
||||
*ngIf="hasDependentViolation"
|
||||
>
|
||||
<ion-text color="warning">Will Stop</ion-text>
|
||||
</div>
|
||||
<ion-item
|
||||
style="--ion-item-background: rgb(0,0,0,0); margin-top: 5px"
|
||||
*ngFor="let dep of dependentBreakages"
|
||||
>
|
||||
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
|
||||
<img [src]="dep.iconURL | iconParse" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h5>{{dep.title}}</h5>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="$loading$ | async" class="center-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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { DependentsComponent } from './dependents.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'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DependentsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
InformationPopoverComponentModule,
|
||||
],
|
||||
exports: [DependentsComponent],
|
||||
})
|
||||
export class DependentsComponentModule { }
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { BehaviorSubject, from, Subject } from 'rxjs'
|
||||
import { takeUntil, tap } from 'rxjs/operators'
|
||||
import { DependentBreakage } from 'src/app/models/app-types'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||
import { Colorable, Loadable } from '../loadable'
|
||||
import { WizardAction } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'dependents',
|
||||
templateUrl: './dependents.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
})
|
||||
export class DependentsComponent implements OnInit, Loadable, Colorable {
|
||||
@Input() params: {
|
||||
title: string,
|
||||
action: WizardAction, //Are you sure you want to *uninstall*...,
|
||||
verb: string, // *Uninstalling* will cause problems...
|
||||
fetchBreakages: () => Promise<DependentBreakage[]>,
|
||||
skipConfirmationDialogue?: boolean
|
||||
}
|
||||
@Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise<any>
|
||||
|
||||
|
||||
dependentBreakages: DependentBreakage[]
|
||||
hasDependentViolation: boolean
|
||||
longMessage: string | null = null
|
||||
$color$ = new BehaviorSubject('medium') // this will display disabled while loading
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$cancel$ = new Subject<void>()
|
||||
|
||||
constructor () { }
|
||||
ngOnInit () { }
|
||||
|
||||
load () {
|
||||
this.$color$.next('medium')
|
||||
markAsLoadingDuring$(this.$loading$, from(this.params.fetchBreakages())).pipe(
|
||||
takeUntil(this.$cancel$),
|
||||
tap(breakages => this.dependentBreakages = breakages || []),
|
||||
).subscribe(
|
||||
{
|
||||
complete: () => {
|
||||
this.hasDependentViolation = this.dependentBreakages && this.dependentBreakages.length > 0
|
||||
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.$color$.next('warning')
|
||||
} else if (this.params.skipConfirmationDialogue) {
|
||||
this.finished({ })
|
||||
} else {
|
||||
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
|
||||
this.$color$.next('success')
|
||||
}
|
||||
},
|
||||
error: (e: Error) => this.finished({ error: new Error(`Fetching dependent service information failed: ${e.message || e}`) }),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<ion-header style="height: 12vh">
|
||||
<ion-toolbar>
|
||||
<ion-label class="toolbar-label text-ellipses">
|
||||
<h1 class="toolbar-title">{{ params.toolbar.title }}</h1>
|
||||
<h3 style="font-size: large; font-style: italic">{{params.toolbar.action}} <ion-text style="font-size: medium;" color="medium">{{ params.toolbar.version | displayEmver }}</ion-text></h3>
|
||||
</ion-label>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
|
||||
<ion-slide *ngFor="let slide of params.slideDefinitions">
|
||||
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
|
||||
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
|
||||
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
|
||||
</ion-slide>
|
||||
</ion-slides>
|
||||
|
||||
|
||||
<div *ngIf="$error$ | async as error" class="slide-content">
|
||||
<div style="margin-top: 25px;">
|
||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
|
||||
Error
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="long-message">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar style="padding: 8px;">
|
||||
<ng-container *ngIf="!($error$ | async)">
|
||||
<ion-button slot="start" *ngIf="($anythingLoading$ | async) && currentSlideDef.cancelButton.whileLoading as cancel" (click)="finished({ cancelled: true })" class="toolbar-button" fill="outline" color="medium">
|
||||
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="start" *ngIf="!($anythingLoading$ | async) && currentSlideDef.cancelButton.afterLoading as cancel" (click)="finished({ cancelled: true })" class="toolbar-button" fill="outline" color="medium">
|
||||
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="end" *ngIf="currentSlideDef.nextButton as nextButton" (click)="finished({})" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="$error$ | async">
|
||||
<ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
||||
</ng-container>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { InstallWizardComponent } from './install-wizard.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
|
||||
import { DependentsComponentModule } from './dependents/dependents.component.module'
|
||||
import { CompleteComponentModule } from './complete/complete.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
InstallWizardComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
DependenciesComponentModule,
|
||||
DependentsComponentModule,
|
||||
CompleteComponentModule,
|
||||
],
|
||||
exports: [InstallWizardComponent],
|
||||
})
|
||||
export class InstallWizardComponentModule { }
|
||||
@@ -0,0 +1,79 @@
|
||||
.toolbar-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: white;
|
||||
padding: 8px 0px 8px 15px;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-size: x-large;
|
||||
text-transform: capitalize;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #404040;
|
||||
font-family: 'Montserrat';
|
||||
}
|
||||
|
||||
.center-spinner {
|
||||
min-height: 40vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
color:white;
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
color:white;
|
||||
min-height: 40vh
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.long-message {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding: 10px;
|
||||
font-size: small;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
}
|
||||
|
||||
@media (min-width:500px) {
|
||||
.long-message {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding: 10px;
|
||||
font-size: medium;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
text-transform: capitalize;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.smaller-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
width: 2vh;
|
||||
height: 2vh;
|
||||
border-radius: 50px;
|
||||
left: -0.75vh;
|
||||
top: -0.75vh;
|
||||
}
|
||||
128
ui/src/app/components/install-wizard/install-wizard.component.ts
Normal file
128
ui/src/app/components/install-wizard/install-wizard.component.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||
import { CompleteComponent } from './complete/complete.component'
|
||||
import { DependenciesComponent } from './dependencies/dependencies.component'
|
||||
import { DependentsComponent } from './dependents/dependents.component'
|
||||
import { Colorable, Loadable } from './loadable'
|
||||
import { WizardAction } from './wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'install-wizard',
|
||||
templateUrl: './install-wizard.component.html',
|
||||
styleUrls: ['./install-wizard.component.scss'],
|
||||
})
|
||||
export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
@Input() params: {
|
||||
// defines the slideshow in the html
|
||||
slideDefinitions: SlideDefinition[]
|
||||
toolbar: TopbarParams
|
||||
}
|
||||
|
||||
// containers
|
||||
@ViewChild(IonContent) contentContainer: IonContent
|
||||
@ViewChild(IonSlides) slideContainer: IonSlides
|
||||
|
||||
//don't use this, use slideComponents instead.
|
||||
@ViewChildren('components')
|
||||
public slideComponentsQL: QueryList<Loadable & Colorable>
|
||||
|
||||
//don't use this, use currentSlide instead.
|
||||
slideIndex = 0
|
||||
|
||||
get slideComponents (): (Loadable & Colorable)[] {
|
||||
return this.slideComponentsQL.toArray()
|
||||
}
|
||||
|
||||
get currentSlide (): (Loadable & Colorable) {
|
||||
return this.slideComponents[this.slideIndex]
|
||||
}
|
||||
|
||||
get currentSlideDef (): SlideDefinition {
|
||||
return this.params.slideDefinitions[this.slideIndex]
|
||||
}
|
||||
|
||||
$anythingLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true)
|
||||
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
|
||||
$error$ = new BehaviorSubject(undefined)
|
||||
|
||||
constructor (private readonly modalController: ModalController) { super() }
|
||||
ngOnInit () { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.currentSlide.load()
|
||||
this.slideContainer.update()
|
||||
this.slideContainer.lockSwipes(true)
|
||||
}
|
||||
|
||||
ionViewDidEnter () {
|
||||
this.cleanup(
|
||||
combineLatest(this.slideComponents.map(component => component.$loading$)).pipe(
|
||||
map(loadings => !loadings.every(p => !p)),
|
||||
).subscribe(this.$anythingLoading$),
|
||||
combineLatest(this.slideComponents.map(component => component.$color$)).pipe(
|
||||
map(colors => colors[this.slideIndex]),
|
||||
).subscribe(this.$currentColor$),
|
||||
)
|
||||
}
|
||||
|
||||
finished = (info: { error?: Error, cancelled?: true, final?: true }) => {
|
||||
if (info.cancelled) this.currentSlide.$cancel$.next()
|
||||
if (info.final || info.cancelled) return this.modalController.dismiss(info)
|
||||
if (info.error) return this.$error$.next(capitalizeFirstLetter(info.error.message))
|
||||
|
||||
this.slide()
|
||||
}
|
||||
|
||||
private async slide () {
|
||||
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
|
||||
this.slideIndex += 1
|
||||
await this.slideContainer.lockSwipes(false)
|
||||
await Promise.all([this.contentContainer.scrollToTop(), this.slideContainer.slideNext()])
|
||||
await this.slideContainer.lockSwipes(true)
|
||||
this.currentSlide.load()
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlideCommon {
|
||||
selector: string
|
||||
cancelButton: {
|
||||
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
|
||||
afterLoading?: { text?: string },
|
||||
whileLoading?: { text?: string }
|
||||
}
|
||||
nextButton?: string,
|
||||
finishButton?: string
|
||||
}
|
||||
|
||||
export type SlideDefinition = SlideCommon & (
|
||||
{
|
||||
selector: 'dependencies',
|
||||
params: DependenciesComponent['params']
|
||||
} | {
|
||||
selector: 'dependents',
|
||||
params: DependentsComponent['params']
|
||||
} | {
|
||||
selector: 'complete',
|
||||
params: CompleteComponent['params']
|
||||
}
|
||||
)
|
||||
|
||||
export type TopbarParams = { action: WizardAction, title: string, version: string }
|
||||
|
||||
export async function wizardModal (
|
||||
modalController: ModalController, params: InstallWizardComponent['params'],
|
||||
): Promise<{ cancelled?: true, final?: true, modal: HTMLIonModalElement }> {
|
||||
const modal = await modalController.create({
|
||||
backdropDismiss: false,
|
||||
cssClass: 'wizard-modal',
|
||||
component: InstallWizardComponent,
|
||||
componentProps: { params },
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
return modal.onWillDismiss().then(({ data }) => ({ ...data, modal }))
|
||||
}
|
||||
11
ui/src/app/components/install-wizard/loadable.ts
Normal file
11
ui/src/app/components/install-wizard/loadable.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BehaviorSubject, Subject } from 'rxjs'
|
||||
|
||||
export interface Loadable {
|
||||
load: () => void
|
||||
$loading$: BehaviorSubject<boolean> //will be true during load function
|
||||
$cancel$: Subject<void> //will cancel load function
|
||||
}
|
||||
|
||||
export interface Colorable {
|
||||
$color$: BehaviorSubject<string>
|
||||
}
|
||||
161
ui/src/app/components/install-wizard/prebaked-wizards.ts
Normal file
161
ui/src/app/components/install-wizard/prebaked-wizards.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
||||
import { ApiService } from '../../services/api/api.service'
|
||||
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WizardBaker {
|
||||
constructor (private readonly apiService: ApiService, private readonly appModel: AppModel) { }
|
||||
|
||||
install (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[]
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, serviceRequirements } = values
|
||||
|
||||
validate(id, exists, 'missing id')
|
||||
validate(title, exists, 'missing title')
|
||||
validate(version, exists, 'missing version')
|
||||
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
|
||||
|
||||
const action = 'install'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
}},
|
||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
||||
action, verb: 'beginning installation for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
||||
this.appModel.add({ ...app, status: AppStatus.INSTALLING })
|
||||
}),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
update (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[]
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, serviceRequirements } = values
|
||||
validate(id, exists, 'missing id')
|
||||
validate(title, exists, 'missing title')
|
||||
validate(version, exists, 'missing version')
|
||||
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
|
||||
|
||||
const action = 'update'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
}},
|
||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Update Anyways', params: {
|
||||
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
||||
}},
|
||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
||||
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
||||
}),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
downgrade (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[]
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, serviceRequirements } = values
|
||||
|
||||
validate(id, exists, 'missing id')
|
||||
validate(title, exists, 'missing title')
|
||||
validate(version, exists, 'missing version')
|
||||
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
|
||||
|
||||
const action = 'downgrade'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
}},
|
||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade Anyways', params: {
|
||||
skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
||||
}},
|
||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
||||
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
||||
}),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
uninstall (values: {
|
||||
id: string, title: string, version: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version } = values
|
||||
|
||||
validate(id, exists, 'missing id')
|
||||
validate(title, exists, 'missing title')
|
||||
validate(version, exists, 'missing version')
|
||||
|
||||
const action = 'uninstall'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: {
|
||||
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
|
||||
}},
|
||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
||||
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
stop (values: {
|
||||
breakages: DependentBreakage[], id: string, title: string, version: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { breakages, title, version } = values
|
||||
|
||||
validate(breakages, t => !!t && Array.isArray(t), 'missing breakages')
|
||||
validate(title, exists, 'missing title')
|
||||
validate(version, exists, 'missing version')
|
||||
|
||||
const action = 'stop'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Stop Anyways', params: {
|
||||
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
configure (values: {
|
||||
breakages: DependentBreakage[], app: AppInstalledPreview
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { breakages, app } = values
|
||||
const { title, versionInstalled: version } = app
|
||||
const action = 'configure'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Save Config Anyways', params: {
|
||||
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
}
|
||||
|
||||
function validate<T> (t: T, test: (t: T) => Boolean, desc: string) {
|
||||
if (!test(t)) {
|
||||
console.error('failed validation', desc, t)
|
||||
throw new Error(desc)
|
||||
}
|
||||
}
|
||||
|
||||
const exists = t => !!t
|
||||
7
ui/src/app/components/install-wizard/wizard-types.ts
Normal file
7
ui/src/app/components/install-wizard/wizard-types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type WizardAction =
|
||||
'install'
|
||||
| 'update'
|
||||
| 'downgrade'
|
||||
| 'uninstall'
|
||||
| 'stop'
|
||||
| 'configure'
|
||||
@@ -0,0 +1,16 @@
|
||||
<ion-item button (click)="handleClick()">
|
||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'Added'" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
|
||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'NoChange'" style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);" name="ellipse"></ion-icon>
|
||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'Edited'" style="margin-right: 15px" color="primary" name="ellipse"></ion-icon>
|
||||
<ion-icon size="small" slot="start" *ngIf="anno | annotationStatus: 'Invalid'" style="margin-right: 15px" color="danger" name="warning-outline"></ion-icon>
|
||||
|
||||
<div class="organizer">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<ion-text color="medium">{{ spec.name }}</ion-text>
|
||||
<ion-text class="new-tag" *ngIf="anno | annotationStatus: 'Added'">(new)</ion-text>
|
||||
</ion-label>
|
||||
</div>
|
||||
|
||||
<ion-note *ngIf="displayValue" style="font-size: small;" [class.bold]="anno | annotationStatus: 'Edited'" slot="end">{{ displayValue }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div *ngFor="let keyval of (spec.type === 'object' ? spec.spec : spec.variants[value[spec.tag.id]]) | keyvalue: asIsOrder">
|
||||
<object-config-item
|
||||
[key]="keyval.key"
|
||||
[spec]="keyval.value"
|
||||
[value]="value[keyval.key]"
|
||||
[anno]="annotations.members[keyval.key]"
|
||||
(onClick)="handleClick(keyval.key)"
|
||||
[class.add-margin]="keyval.key === 'advanced'"
|
||||
></object-config-item>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ObjectConfigComponent, ObjectConfigItemComponent } from './object-config.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ObjectConfigComponent,
|
||||
ObjectConfigItemComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
],
|
||||
exports: [
|
||||
ObjectConfigComponent,
|
||||
ObjectConfigItemComponent,
|
||||
],
|
||||
})
|
||||
export class ObjectConfigComponentModule { }
|
||||
@@ -0,0 +1,43 @@
|
||||
.add-margin {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.help-button {
|
||||
position: relative;
|
||||
bottom: 7px;
|
||||
right: 9px;
|
||||
}
|
||||
|
||||
.new-tag {
|
||||
padding: 0px 5px;
|
||||
font-weight: bold;
|
||||
font-size: smaller;
|
||||
color: #cecece;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-icon{
|
||||
// width: 2%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
color: var(--ion-color-danger) !important;
|
||||
}
|
||||
|
||||
.organizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
101
ui/src/app/components/object-config/object-config.component.ts
Normal file
101
ui/src/app/components/object-config/object-config.component.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Annotation, Annotations } from '../../app-config/config-utilities'
|
||||
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
|
||||
import { ConfigCursor } from 'src/app/app-config/config-cursor'
|
||||
import { ModalPresentable } from 'src/app/app-config/modal-presentable'
|
||||
import { ValueSpecOf, ValueSpec } from 'src/app/app-config/config-types'
|
||||
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'object-config',
|
||||
templateUrl: './object-config.component.html',
|
||||
styleUrls: ['./object-config.component.scss'],
|
||||
})
|
||||
export class ObjectConfigComponent extends ModalPresentable {
|
||||
@Input() cursor: ConfigCursor<'object' | 'union'>
|
||||
@Output() onEdit = new EventEmitter<boolean>()
|
||||
spec: ValueSpecOf<'object' | 'union'>
|
||||
value: object
|
||||
annotations: Annotations<'object' | 'union'>
|
||||
|
||||
constructor (
|
||||
trackingModalCtrl: TrackingModalController,
|
||||
) {
|
||||
super(trackingModalCtrl)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.spec = this.cursor.spec()
|
||||
this.value = this.cursor.config()
|
||||
this.annotations = this.cursor.getAnnotations()
|
||||
}
|
||||
|
||||
async handleClick (key: string) {
|
||||
const nextCursor = this.cursor.seekNext(key)
|
||||
nextCursor.createFirstEntryForList()
|
||||
|
||||
await this.presentModal(nextCursor, () => {
|
||||
this.onEdit.emit(true)
|
||||
this.annotations = this.cursor.getAnnotations()
|
||||
})
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'object-config-item',
|
||||
templateUrl: './object-config-item.component.html',
|
||||
styleUrls: ['./object-config.component.scss'],
|
||||
})
|
||||
|
||||
export class ObjectConfigItemComponent {
|
||||
@Input() key: string
|
||||
@Input() spec: ValueSpec
|
||||
@Input() value: string | number
|
||||
@Input() anno: Annotation
|
||||
@Output() onClick = new EventEmitter<boolean>()
|
||||
maskPipe: MaskPipe = new MaskPipe()
|
||||
|
||||
displayValue?: string | number | boolean
|
||||
|
||||
ngOnChanges () {
|
||||
switch (this.spec.type) {
|
||||
case 'string':
|
||||
if (this.value) {
|
||||
if (this.spec.masked) {
|
||||
this.displayValue = this.maskPipe.transform(this.value as string, 4)
|
||||
} else {
|
||||
this.displayValue = this.value
|
||||
}
|
||||
} else {
|
||||
this.displayValue = '-'
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
this.displayValue = String(this.value)
|
||||
break
|
||||
case 'number':
|
||||
this.displayValue = this.value || '-'
|
||||
if (this.displayValue && this.spec.units) {
|
||||
this.displayValue = `${this.displayValue} ${this.spec.units}`
|
||||
}
|
||||
break
|
||||
case 'enum':
|
||||
this.displayValue = this.spec.valueNames[this.value]
|
||||
break
|
||||
case 'pointer':
|
||||
this.displayValue = 'System Defined'
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async handleClick (): Promise<void> {
|
||||
if (this.spec.type === 'pointer') return
|
||||
this.onClick.emit(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<ion-button (click)="navigateBack()">
|
||||
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { PwaBackComponent } from './pwa-back.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
PwaBackComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [PwaBackComponent],
|
||||
})
|
||||
export class PwaBackComponentModule { }
|
||||
16
ui/src/app/components/pwa-back-button/pwa-back.component.ts
Normal file
16
ui/src/app/components/pwa-back-button/pwa-back.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { PwaBackService } from 'src/app/services/pwa-back.service'
|
||||
@Component({
|
||||
selector: 'pwa-back-button',
|
||||
templateUrl: './pwa-back.component.html',
|
||||
styleUrls: ['./pwa-back.component.scss'],
|
||||
})
|
||||
export class PwaBackComponent {
|
||||
constructor (
|
||||
private readonly pwaBack: PwaBackService,
|
||||
) { }
|
||||
|
||||
navigateBack () {
|
||||
return this.pwaBack.back()
|
||||
}
|
||||
}
|
||||
1
ui/src/app/components/qr/qr.component.html
Normal file
1
ui/src/app/components/qr/qr.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<qrcode [qrdata]="text" [width]="width" errorCorrectionLevel="L"></qrcode>
|
||||
18
ui/src/app/components/qr/qr.component.module.ts
Normal file
18
ui/src/app/components/qr/qr.component.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { QRComponent } from './qr.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { QRCodeModule } from 'angularx-qrcode'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
QRComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
QRCodeModule,
|
||||
],
|
||||
exports: [QRComponent],
|
||||
})
|
||||
export class QRComponentModule { }
|
||||
0
ui/src/app/components/qr/qr.component.scss
Normal file
0
ui/src/app/components/qr/qr.component.scss
Normal file
16
ui/src/app/components/qr/qr.component.ts
Normal file
16
ui/src/app/components/qr/qr.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { isPlatform } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'qr',
|
||||
templateUrl: './qr.component.html',
|
||||
styleUrls: ['./qr.component.scss'],
|
||||
})
|
||||
export class QRComponent {
|
||||
@Input() text: string
|
||||
width: number
|
||||
|
||||
ngOnInit () {
|
||||
this.width = isPlatform('ios') || isPlatform('android') ? 320 : 420
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<ion-fab id="recommendation" vertical="top" horizontal="end" slot="fixed">
|
||||
<ion-fab-button [disabled]="disabled" style="border-style: solid;
|
||||
border-radius: 100px;
|
||||
border-color: #FFEB3B;
|
||||
border-width: medium;
|
||||
box-shadow: 0 0 10px white;" size="small" (click)="presentPopover($event)">
|
||||
<img [src]="rec.iconURL | iconParse" />
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { RecommendationButtonComponent } from './recommendation-button.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RecommendationButtonComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [RecommendationButtonComponent],
|
||||
})
|
||||
export class RecommendationButtonComponentModule { }
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { PopoverController } from '@ionic/angular'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||
import { InformationPopoverComponent } from '../information-popover/information-popover.component'
|
||||
|
||||
@Component({
|
||||
selector: 'recommendation-button',
|
||||
templateUrl: './recommendation-button.component.html',
|
||||
styleUrls: ['./recommendation-button.component.scss'],
|
||||
})
|
||||
export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
||||
@Input() rec: Recommendation
|
||||
@Input() raise?: { id: string }
|
||||
constructor (private readonly router: Router, private readonly popoverController: PopoverController) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
if (!this.raise) return
|
||||
const mainContent = document.getElementsByTagName('ion-app')[0]
|
||||
const recButton = document.getElementById(this.raise.id)
|
||||
mainContent.appendChild(recButton)
|
||||
|
||||
this.router.events.pipe(filter(e => !!(e as any).urlAfterRedirects, take(1))).subscribe((e: any) => {
|
||||
recButton.remove()
|
||||
})
|
||||
}
|
||||
|
||||
disabled = false
|
||||
|
||||
async presentPopover (ev: any) {
|
||||
const popover = await this.popoverController.create({
|
||||
component: InformationPopoverComponent,
|
||||
event: ev,
|
||||
translucent: false,
|
||||
showBackdrop: true,
|
||||
backdropDismiss: true,
|
||||
componentProps: {
|
||||
information: `
|
||||
<div style="font-size: medium; font-style: italic; margin: 5px 0px;">
|
||||
${capitalizeFirstLetter(this.rec.title)} Installation Recommendations
|
||||
</div>
|
||||
<div>
|
||||
${this.rec.description}
|
||||
</div>`,
|
||||
},
|
||||
})
|
||||
popover.onWillDismiss().then(() => {
|
||||
this.disabled = false
|
||||
})
|
||||
this.disabled = true
|
||||
return await popover.present()
|
||||
}
|
||||
}
|
||||
|
||||
export type Recommendation = {
|
||||
title: string
|
||||
appId: string
|
||||
iconURL: string,
|
||||
description: string,
|
||||
versionSpec?: string
|
||||
whyDependency?: string
|
||||
}
|
||||
24
ui/src/app/components/status/status.component.html
Normal file
24
ui/src/app/components/status/status.component.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<p *ngIf="size === 'small'" style="margin: 0 0 4px 0;">
|
||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
||||
</p>
|
||||
|
||||
<h3 *ngIf="size === 'italics-small'" style="margin: 0 0 4px 0;">
|
||||
<ion-text [style]="style" style="font-size: small; font-style: italic; text-transform: lowercase;" [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
||||
</h3>
|
||||
|
||||
<h3 *ngIf="size === 'medium'">
|
||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots dots-medium" name="dots" [color]="color"></ion-spinner>
|
||||
</h3>
|
||||
|
||||
<h1 *ngIf="size === 'large'">
|
||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
||||
</h1>
|
||||
|
||||
<h1 style="font-size: 18px; font-weight: 500" *ngIf="size === 'bold-large'">
|
||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
||||
</h1>
|
||||
16
ui/src/app/components/status/status.component.module.ts
Normal file
16
ui/src/app/components/status/status.component.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { StatusComponent } from './status.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
StatusComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [StatusComponent],
|
||||
})
|
||||
export class StatusComponentModule { }
|
||||
32
ui/src/app/components/status/status.component.scss
Normal file
32
ui/src/app/components/status/status.component.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.icon-small {
|
||||
width: auto;
|
||||
height: 14px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.icon-medium {
|
||||
width: auto;
|
||||
height: 18px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.icon-large {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
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;
|
||||
}
|
||||
56
ui/src/app/components/status/status.component.ts
Normal file
56
ui/src/app/components/status/status.component.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { AppStatus } from 'src/app/models/app-model'
|
||||
import { ServerStatus } from 'src/app/models/server-model'
|
||||
import { ServerStatusRendering, AppStatusRendering } from '../../util/status-rendering'
|
||||
|
||||
@Component({
|
||||
selector: 'status',
|
||||
templateUrl: './status.component.html',
|
||||
styleUrls: ['./status.component.scss'],
|
||||
})
|
||||
export class StatusComponent {
|
||||
@Input() appStatus?: AppStatus
|
||||
@Input() serverStatus?: ServerStatus
|
||||
@Input() size: 'small' | 'medium' | 'large' | 'italics-small' | 'bold-large' = 'large'
|
||||
@Input() text: string = ''
|
||||
color: string
|
||||
display: string
|
||||
showDots: boolean
|
||||
style = ''
|
||||
|
||||
ngOnChanges () {
|
||||
if (this.serverStatus) {
|
||||
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.color = color
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user