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
@@ -1,26 +0,0 @@
|
||||
<!-- TODO: EJECT-DISKS, add a check box to allow a user to eject a disk on backup completion. -->
|
||||
<ion-content>
|
||||
<div style="height: 85%; margin: 20px; display: flex; flex-direction: column; justify-content: space-between;">
|
||||
<div>
|
||||
<h4><ion-text color="dark">Ready to Backup</ion-text></h4>
|
||||
<p><ion-text color="medium">Enter your master password to create an encrypted backup.</ion-text></p>
|
||||
</div>
|
||||
<div>
|
||||
<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-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-item>
|
||||
<ion-item *ngIf="$error$ | async as e" lines="none" style="--background: var(--ion-background-color);">
|
||||
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{e}}</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; align-items: center;">
|
||||
<ion-button fill="clear" color="medium" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button fill="clear" color="primary" (click)="submit()">
|
||||
Create Backup
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -1,22 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { AppBackupConfirmationComponent } from './app-backup-confirmation.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppBackupConfirmationComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
FormsModule,
|
||||
],
|
||||
exports: [AppBackupConfirmationComponent],
|
||||
})
|
||||
export class AppBackupConfirmationComponentModule { }
|
||||
@@ -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;">
|
||||
<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-badge mode="md" class="md-badge" *ngIf="(badge$ | ngrxPush) && !(menuFixedOpen$ | ngrxPush)" color="danger">{{ badge$ | ngrxPush }}</ion-badge>
|
||||
<ion-menu-button color="dark"></ion-menu-button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
.ios-badge {
|
||||
background-color: var(--ion-color-start9);
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 62%;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
// .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;
|
||||
top: -8px;
|
||||
left: 56%;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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'
|
||||
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
|
||||
|
||||
@Component({
|
||||
selector: 'badge-menu-button',
|
||||
@@ -12,16 +10,14 @@ import { isPlatform } from '@ionic/angular'
|
||||
})
|
||||
|
||||
export class BadgeMenuComponent {
|
||||
badge$: Observable<boolean>
|
||||
badge$: Observable<number>
|
||||
menuFixedOpen$: Observable<boolean>
|
||||
isIos: boolean
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly patch: PatchDbModel,
|
||||
) {
|
||||
this.menuFixedOpen$ = this.splitPane.$menuFixedOpenOnLeft$.asObservable()
|
||||
this.badge$ = this.serverModel.watch().badge.pipe(map(i => i > 0))
|
||||
this.isIos = isPlatform('ios')
|
||||
this.menuFixedOpen$ = this.splitPane.menuFixedOpenOnLeft$.asObservable()
|
||||
this.badge$ = this.patch.watch$('server-info', 'unread-notification-count')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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({
|
||||
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: 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}}
|
||||
</ion-label>
|
||||
</div>
|
||||
@@ -11,7 +11,7 @@
|
||||
</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-label class="long-message">{{label}}</ion-label>
|
||||
</div>
|
||||
@@ -28,17 +28,18 @@ export class CompleteComponent implements OnInit, Loadable {
|
||||
}
|
||||
|
||||
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$color$ = new BehaviorSubject('medium')
|
||||
$cancel$ = new Subject<void>()
|
||||
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.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
|
||||
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}`)),
|
||||
complete: () => this.params.skipCompletionDialogue && this.transitions.final(),
|
||||
},
|
||||
)
|
||||
@@ -50,37 +51,37 @@ export class CompleteComponent implements OnInit, Loadable {
|
||||
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.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.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.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.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.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.color$.next('success')
|
||||
this.successText = 'Success'
|
||||
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 *ngIf="!($loading$ | async)" class="slide-content">
|
||||
<div *ngIf="!(loading$ | ngrxPush)" 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;"
|
||||
@@ -26,7 +26,7 @@
|
||||
*ngFor="let dep of dependentBreakages"
|
||||
>
|
||||
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
|
||||
<img [src]="dep.iconURL | iconParse" />
|
||||
<img [src]="dep.iconURL" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h5>{{dep.title}}</h5>
|
||||
@@ -35,7 +35,7 @@
|
||||
</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-label class="long-message">Checking for installed services which depend on {{params.title}}...</ion-label>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { Breakages } from 'src/app/services/api/api-types'
|
||||
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 { WizardAction } from '../wizard-types'
|
||||
|
||||
@@ -17,7 +17,7 @@ export class DependentsComponent implements OnInit, Loadable {
|
||||
title: string,
|
||||
action: WizardAction, //Are you sure you want to *uninstall*...,
|
||||
verb: string, // *Uninstalling* will cause problems...
|
||||
fetchBreakages: () => Promise<DependentBreakage[]>,
|
||||
fetchBreakages: () => Promise<Breakages>,
|
||||
skipConfirmationDialogue?: boolean
|
||||
}
|
||||
@Input() transitions: {
|
||||
@@ -27,34 +27,33 @@ export class DependentsComponent implements OnInit, Loadable {
|
||||
error: (e: Error) => void
|
||||
}
|
||||
|
||||
|
||||
dependentBreakages: DependentBreakage[]
|
||||
dependentBreakages: Breakages
|
||||
hasDependentViolation: boolean
|
||||
longMessage: string | null = null
|
||||
$color$ = new BehaviorSubject('medium') // this will display disabled while loading
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$cancel$ = new Subject<void>()
|
||||
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 || []),
|
||||
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
|
||||
this.hasDependentViolation = this.dependentBreakages && !isEmptyObject(this.dependentBreakages)
|
||||
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')
|
||||
this.color$.next('warning')
|
||||
} else if (this.params.skipConfirmationDialogue) {
|
||||
this.transitions.next()
|
||||
} else {
|
||||
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}`)),
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
</ion-header>
|
||||
|
||||
<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">
|
||||
<!-- 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>
|
||||
<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>
|
||||
<complete #components *ngIf="def.slide.selector === 'complete'" [params]="def.slide.params" [transitions]="transitions"></complete>
|
||||
</ion-slide>
|
||||
</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: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<ion-footer>
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
</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>
|
||||
</ng-container>
|
||||
</ion-toolbar>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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'
|
||||
import { NotesComponentModule } from './notes/notes.component.module'
|
||||
@@ -18,7 +17,6 @@ import { NotesComponentModule } from './notes/notes.component.module'
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
DependenciesComponentModule,
|
||||
DependentsComponentModule,
|
||||
CompleteComponentModule,
|
||||
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 { BehaviorSubject } from 'rxjs'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { capitalizeFirstLetter, pauseFor } 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 { NotesComponent } from './notes/notes.component'
|
||||
import { Loadable } from './loadable'
|
||||
@@ -15,7 +13,7 @@ import { WizardAction } from './wizard-types'
|
||||
templateUrl: './install-wizard.component.html',
|
||||
styleUrls: ['./install-wizard.component.scss'],
|
||||
})
|
||||
export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
export class InstallWizardComponent {
|
||||
@Input() params: {
|
||||
// defines each slide along with bottom bar
|
||||
slideDefinitions: SlideDefinition[]
|
||||
@@ -40,11 +38,13 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
return this.params.slideDefinitions[this.slideIndex].bottomBar
|
||||
}
|
||||
|
||||
$initializing$ = new BehaviorSubject(true)
|
||||
$error$ = new BehaviorSubject(undefined)
|
||||
initializing$ = new BehaviorSubject(true)
|
||||
error$ = new BehaviorSubject(undefined)
|
||||
|
||||
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
|
||||
ngOnInit () { }
|
||||
constructor (
|
||||
private readonly modalController: ModalController,
|
||||
private readonly zone: NgZone,
|
||||
) { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.currentSlide.load()
|
||||
@@ -53,15 +53,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
}
|
||||
|
||||
ionViewDidEnter () {
|
||||
this.$initializing$.next(false)
|
||||
this.initializing$.next(false)
|
||||
}
|
||||
|
||||
// process bottom bar buttons
|
||||
private transition = (info: { 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.error) return this.$error$.next(capitalizeFirstLetter(i.error.message))
|
||||
if (i.error) return this.error$.next(capitalizeFirstLetter(i.error.message))
|
||||
|
||||
this.moveToNextSlide(i.next)
|
||||
}
|
||||
@@ -90,7 +90,6 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
|
||||
export interface SlideDefinition {
|
||||
slide:
|
||||
{ selector: 'dependencies', params: DependenciesComponent['params'] } |
|
||||
{ selector: 'dependents', params: DependentsComponent['params'] } |
|
||||
{ selector: 'complete', params: CompleteComponent['params'] } |
|
||||
{ selector: 'notes', params: NotesComponent['params'] }
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BehaviorSubject, Subject } from 'rxjs'
|
||||
export interface Loadable {
|
||||
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.
|
||||
$loading$: BehaviorSubject<boolean> // will be true during load function
|
||||
$cancel$: Subject<void> // will cancel load function
|
||||
loading$: BehaviorSubject<boolean> // will be true during load function
|
||||
cancel$: Subject<void> // will cancel load function
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ export class NotesComponent implements OnInit, Loadable {
|
||||
titleColor: string
|
||||
}
|
||||
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$cancel$ = new Subject<void>()
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
|
||||
load () { }
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||
import { Breakages } from 'src/app/services/api/api-types'
|
||||
import { exists } from 'src/app/util/misc.util'
|
||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
||||
import { ApiService } from '../../services/api/api.service'
|
||||
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
||||
import { WizardAction } from './wizard-types'
|
||||
@@ -11,43 +10,34 @@ import { WizardAction } from './wizard-types'
|
||||
export class WizardBaker {
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly updateService: OsUpdateService,
|
||||
private readonly appModel: AppModel,
|
||||
) { }
|
||||
|
||||
install (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
id: string, title: string, version: string, installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, serviceRequirements, installAlert } = values
|
||||
const { id, title, version, installAlert } = 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 toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? {
|
||||
slide: {
|
||||
selector: 'notes',
|
||||
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
|
||||
params: {
|
||||
notes: installAlert,
|
||||
title: 'Warning',
|
||||
titleColor: 'warning',
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next',
|
||||
},
|
||||
} : undefined,
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependencies',
|
||||
params: { action, title, version, serviceRequirements },
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } },
|
||||
next: 'Install',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
@@ -55,7 +45,7 @@ export class WizardBaker {
|
||||
action,
|
||||
verb: 'beginning installation for',
|
||||
title,
|
||||
executeAction: () => this.apiService.installApp(id, version).then(app => { this.appModel.add({ ...app, status: AppStatus.INSTALLING })}),
|
||||
executeAction: () => this.apiService.installPackage({ id, version }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
@@ -68,14 +58,13 @@ export class WizardBaker {
|
||||
}
|
||||
|
||||
update (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
id: string, title: string, version: string, installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, serviceRequirements, installAlert } = values
|
||||
const { id, title, version, installAlert } = 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 }
|
||||
@@ -84,26 +73,26 @@ export class WizardBaker {
|
||||
installAlert ? {
|
||||
slide: {
|
||||
selector: 'notes',
|
||||
params: { notes: installAlert, title: 'Warning', titleColor: 'warning'},
|
||||
params: {
|
||||
notes: installAlert,
|
||||
title: 'Warning',
|
||||
titleColor: 'warning',
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } },
|
||||
next: 'Next',
|
||||
},
|
||||
} : undefined,
|
||||
{ slide: {
|
||||
selector: 'dependencies',
|
||||
params: { action, title, version, serviceRequirements },
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } },
|
||||
next: 'Update',
|
||||
},
|
||||
},
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
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: {
|
||||
@@ -111,12 +100,14 @@ export class WizardBaker {
|
||||
next: 'Update Anyways',
|
||||
},
|
||||
},
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
||||
}),
|
||||
action,
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
executeAction: () => this.apiService.installPackage({ id, version }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
@@ -138,18 +129,27 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ slide : {
|
||||
{
|
||||
slide : {
|
||||
selector: 'notes',
|
||||
params: { notes: releaseNotes, title: 'Release Notes', titleColor: 'dark' },
|
||||
params: {
|
||||
notes: releaseNotes,
|
||||
title: 'Release Notes',
|
||||
titleColor: 'dark',
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
|
||||
},
|
||||
},
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
|
||||
action,
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
executeAction: () => this.apiService.updateServer({ }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
@@ -162,14 +162,13 @@ export class WizardBaker {
|
||||
}
|
||||
|
||||
downgrade (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
id: string, title: string, version: string, installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, serviceRequirements, installAlert } = values
|
||||
const { id, title, version, installAlert } = 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 }
|
||||
@@ -178,23 +177,22 @@ export class WizardBaker {
|
||||
installAlert ? {
|
||||
slide: {
|
||||
selector: 'notes',
|
||||
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
|
||||
params: {
|
||||
notes: installAlert,
|
||||
title: 'Warning',
|
||||
titleColor: 'warning',
|
||||
},
|
||||
},
|
||||
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next' },
|
||||
} : undefined,
|
||||
{ slide: {
|
||||
selector: 'dependencies',
|
||||
params: { action, title, version, serviceRequirements },
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { afterLoading: { text: 'Cancel' } },
|
||||
next: 'Downgrade',
|
||||
},
|
||||
},
|
||||
{ slide: {
|
||||
selector: 'dependents',
|
||||
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: {
|
||||
@@ -204,9 +202,10 @@ export class WizardBaker {
|
||||
{ slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
||||
}),
|
||||
action,
|
||||
verb: 'beginning downgrade for',
|
||||
title,
|
||||
executeAction: () => this.apiService.installPackage({ id, version }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
@@ -231,7 +230,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'notes',
|
||||
params: {
|
||||
notes: uninstallAlert || defaultUninstallationWarning(title),
|
||||
@@ -241,18 +241,26 @@ export class WizardBaker {
|
||||
},
|
||||
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Continue' },
|
||||
},
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
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' },
|
||||
},
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
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: { } } },
|
||||
@@ -262,7 +270,7 @@ export class WizardBaker {
|
||||
}
|
||||
|
||||
stop (values: {
|
||||
breakages: DependentBreakage[], id: string, title: string, version: string
|
||||
breakages: Breakages, id: string, title: string, version: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { breakages, title, version } = values
|
||||
|
||||
@@ -274,10 +282,14 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
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' },
|
||||
@@ -286,19 +298,20 @@ export class WizardBaker {
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
configure (values: {
|
||||
breakages: DependentBreakage[], app: AppInstalledPreview
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { breakages, app } = values
|
||||
const { title, versionInstalled: version } = app
|
||||
configure (values: { breakages: Breakages, pkg: InstalledPackageDataEntry }): InstallWizardComponent['params'] {
|
||||
const { breakages, pkg } = values
|
||||
const { title, version } = pkg.manifest
|
||||
const action = 'configure'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ slide: {
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
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' },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 { 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 { ConfigCursor } from 'src/app/pkg-config/config-cursor'
|
||||
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
|
||||
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
|
||||
import { MaskPipe } from 'src/app/pipes/mask.pipe'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { PwaBackService } from 'src/app/services/pwa-back.service'
|
||||
import { NavController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'pwa-back-button',
|
||||
templateUrl: './pwa-back.component.html',
|
||||
@@ -7,10 +8,10 @@ import { PwaBackService } from 'src/app/services/pwa-back.service'
|
||||
})
|
||||
export class PwaBackComponent {
|
||||
constructor (
|
||||
private readonly pwaBack: PwaBackService,
|
||||
private readonly nav: NavController,
|
||||
) { }
|
||||
|
||||
navigateBack () {
|
||||
return this.pwaBack.back()
|
||||
return this.nav.back()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
border-color: #FFEB3B;
|
||||
border-width: medium;
|
||||
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>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input } 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'
|
||||
|
||||
@@ -11,12 +10,13 @@ import { InformationPopoverComponent } from '../information-popover/information-
|
||||
templateUrl: './recommendation-button.component.html',
|
||||
styleUrls: ['./recommendation-button.component.scss'],
|
||||
})
|
||||
export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
||||
export class RecommendationButtonComponent {
|
||||
@Input() rec: Recommendation
|
||||
@Input() raise?: { id: string }
|
||||
constructor (private readonly router: Router, private readonly popoverController: PopoverController) {
|
||||
super()
|
||||
}
|
||||
constructor (
|
||||
private readonly router: Router,
|
||||
private readonly popoverController: PopoverController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
if (!this.raise) return
|
||||
@@ -41,7 +41,7 @@ export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
||||
componentProps: {
|
||||
information: `
|
||||
<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>
|
||||
${this.rec.description}
|
||||
@@ -57,10 +57,9 @@ export class RecommendationButtonComponent extends Cleanup implements OnInit {
|
||||
}
|
||||
|
||||
export type Recommendation = {
|
||||
title: string
|
||||
appId: string
|
||||
iconURL: string,
|
||||
description: string,
|
||||
versionSpec?: string
|
||||
whyDependency?: string
|
||||
dependentId: string
|
||||
dependentTitle: string
|
||||
dependentIcon: string,
|
||||
description: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<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>
|
||||
</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>
|
||||
<p *ngIf="size === 'italics-small'" style="margin: 0 0 4px 0; font-style: italic;">
|
||||
<ion-text [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
|
||||
</h3>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
</h3>
|
||||
|
||||
<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>
|
||||
</h1>
|
||||
|
||||
<h1 style="font-size: 18px; font-weight: 500" *ngIf="size === 'bold-large'">
|
||||
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
|
||||
<h1 *ngIf="size === 'bold-large'" style="font-size: 18px; font-weight: 500">
|
||||
<ion-text [color]="color">{{ display }}</ion-text>
|
||||
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
|
||||
</h1>
|
||||
|
||||
@@ -15,18 +15,3 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
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'
|
||||
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
|
||||
import { ConnectionState } from 'src/app/services/connection.service'
|
||||
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Component({
|
||||
selector: 'status',
|
||||
@@ -9,48 +9,18 @@ import { ServerStatusRendering, AppStatusRendering } from '../../util/status-ren
|
||||
styleUrls: ['./status.component.scss'],
|
||||
})
|
||||
export class StatusComponent {
|
||||
@Input() appStatus?: AppStatus
|
||||
@Input() serverStatus?: ServerStatus
|
||||
@Input() pkg: PackageDataEntry
|
||||
@Input() connection: ConnectionState
|
||||
@Input() size: 'small' | 'medium' | 'large' | 'italics-small' | 'bold-large' = 'large'
|
||||
@Input() text: string = ''
|
||||
color: string
|
||||
display: string
|
||||
showDots: boolean
|
||||
style = ''
|
||||
display = ''
|
||||
color = ''
|
||||
showDots = false
|
||||
|
||||
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
|
||||
const { display, color, showDots } = renderPkgStatus(this.pkg, this.connection)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user