mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
ui: cleanup wizard
This commit is contained in:
committed by
Aiden McClelland
parent
46643cb3a4
commit
52fc2c4011
@@ -3,7 +3,7 @@ import { BehaviorSubject, from, Subject } from 'rxjs'
|
|||||||
import { takeUntil } from 'rxjs/operators'
|
import { takeUntil } from 'rxjs/operators'
|
||||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||||
import { Colorable, Loadable } from '../loadable'
|
import { Loadable } from '../loadable'
|
||||||
import { WizardAction } from '../wizard-types'
|
import { WizardAction } from '../wizard-types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,7 +11,7 @@ import { WizardAction } from '../wizard-types'
|
|||||||
templateUrl: './complete.component.html',
|
templateUrl: './complete.component.html',
|
||||||
styleUrls: ['../install-wizard.component.scss'],
|
styleUrls: ['../install-wizard.component.scss'],
|
||||||
})
|
})
|
||||||
export class CompleteComponent implements OnInit, Loadable, Colorable {
|
export class CompleteComponent implements OnInit, Loadable {
|
||||||
@Input() params: {
|
@Input() params: {
|
||||||
action: WizardAction
|
action: WizardAction
|
||||||
verb: string //loader verb: '*stopping* ...'
|
verb: string //loader verb: '*stopping* ...'
|
||||||
@@ -20,7 +20,13 @@ export class CompleteComponent implements OnInit, Loadable, Colorable {
|
|||||||
skipCompletionDialogue?: boolean
|
skipCompletionDialogue?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise<any>
|
@Input() transitions: {
|
||||||
|
cancel: () => void
|
||||||
|
next: () => void
|
||||||
|
final: () => void
|
||||||
|
error: (e: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$loading$ = new BehaviorSubject(false)
|
$loading$ = new BehaviorSubject(false)
|
||||||
$color$ = new BehaviorSubject('medium')
|
$color$ = new BehaviorSubject('medium')
|
||||||
@@ -32,8 +38,8 @@ export class CompleteComponent implements OnInit, Loadable, Colorable {
|
|||||||
|
|
||||||
load () {
|
load () {
|
||||||
markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe(
|
markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe(
|
||||||
{ error: e => this.finished({ error: new Error(`${this.params.action} failed: ${e.message || e}`) }),
|
{ error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
|
||||||
complete: () => this.params.skipCompletionDialogue && this.finished( { final: true} ),
|
complete: () => this.params.skipCompletionDialogue && this.transitions.final(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AppStatus } from 'src/app/models/app-model'
|
|||||||
import { AppDependency, DependencyViolationSeverity, getViolationSeverity } from 'src/app/models/app-types'
|
import { AppDependency, DependencyViolationSeverity, getViolationSeverity } from 'src/app/models/app-types'
|
||||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||||
import { InformationPopoverComponent } from '../../information-popover/information-popover.component'
|
import { InformationPopoverComponent } from '../../information-popover/information-popover.component'
|
||||||
import { Colorable, Loadable } from '../loadable'
|
import { Loadable } from '../loadable'
|
||||||
import { WizardAction } from '../wizard-types'
|
import { WizardAction } from '../wizard-types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -13,7 +13,7 @@ import { WizardAction } from '../wizard-types'
|
|||||||
templateUrl: './dependencies.component.html',
|
templateUrl: './dependencies.component.html',
|
||||||
styleUrls: ['../install-wizard.component.scss'],
|
styleUrls: ['../install-wizard.component.scss'],
|
||||||
})
|
})
|
||||||
export class DependenciesComponent implements OnInit, Loadable, Colorable {
|
export class DependenciesComponent implements OnInit, Loadable {
|
||||||
@Input() params: {
|
@Input() params: {
|
||||||
action: WizardAction,
|
action: WizardAction,
|
||||||
title: string,
|
title: string,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { takeUntil, tap } from 'rxjs/operators'
|
|||||||
import { DependentBreakage } from 'src/app/models/app-types'
|
import { DependentBreakage } from 'src/app/models/app-types'
|
||||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||||
import { Colorable, Loadable } from '../loadable'
|
import { Loadable } from '../loadable'
|
||||||
import { WizardAction } from '../wizard-types'
|
import { WizardAction } from '../wizard-types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,7 +12,7 @@ import { WizardAction } from '../wizard-types'
|
|||||||
templateUrl: './dependents.component.html',
|
templateUrl: './dependents.component.html',
|
||||||
styleUrls: ['../install-wizard.component.scss'],
|
styleUrls: ['../install-wizard.component.scss'],
|
||||||
})
|
})
|
||||||
export class DependentsComponent implements OnInit, Loadable, Colorable {
|
export class DependentsComponent implements OnInit, Loadable {
|
||||||
@Input() params: {
|
@Input() params: {
|
||||||
title: string,
|
title: string,
|
||||||
action: WizardAction, //Are you sure you want to *uninstall*...,
|
action: WizardAction, //Are you sure you want to *uninstall*...,
|
||||||
@@ -20,7 +20,12 @@ export class DependentsComponent implements OnInit, Loadable, Colorable {
|
|||||||
fetchBreakages: () => Promise<DependentBreakage[]>,
|
fetchBreakages: () => Promise<DependentBreakage[]>,
|
||||||
skipConfirmationDialogue?: boolean
|
skipConfirmationDialogue?: boolean
|
||||||
}
|
}
|
||||||
@Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise<any>
|
@Input() transitions: {
|
||||||
|
cancel: () => void
|
||||||
|
next: () => void
|
||||||
|
final: () => void
|
||||||
|
error: (e: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dependentBreakages: DependentBreakage[]
|
dependentBreakages: DependentBreakage[]
|
||||||
@@ -46,13 +51,13 @@ export class DependentsComponent implements OnInit, Loadable, Colorable {
|
|||||||
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
|
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
|
||||||
this.$color$.next('warning')
|
this.$color$.next('warning')
|
||||||
} else if (this.params.skipConfirmationDialogue) {
|
} else if (this.params.skipConfirmationDialogue) {
|
||||||
this.finished({ })
|
this.transitions.next()
|
||||||
} else {
|
} else {
|
||||||
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
|
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
|
||||||
this.$color$.next('success')
|
this.$color$.next('success')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (e: Error) => this.finished({ error: new Error(`Fetching dependent service information failed: ${e.message || e}`) }),
|
error: (e: Error) => this.transitions.error(new Error(`Fetching dependent service information failed: ${e.message || e}`)),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
|
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
|
||||||
<ion-slide *ngFor="let slide of params.slideDefinitions">
|
<ion-slide *ngFor="let def of params.slideDefinitions">
|
||||||
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
|
<!-- We can pass [transitions]="transitions" into the component if logic within the component needs to trigger a transition (not just bottom bar) -->
|
||||||
<notes #components *ngIf="slide.selector === 'notes'" [params]="slide.params"></notes>
|
<dependencies #components *ngIf="def.slide.selector === 'dependencies'" [params]="def.slide.params"></dependencies>
|
||||||
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
|
<notes #components *ngIf="def.slide.selector === 'notes'" [params]="def.slide.params"></notes>
|
||||||
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
|
<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-slide>
|
||||||
</ion-slides>
|
</ion-slides>
|
||||||
|
|
||||||
@@ -34,20 +35,31 @@
|
|||||||
|
|
||||||
<ion-footer>
|
<ion-footer>
|
||||||
<ion-toolbar style="padding: 8px;">
|
<ion-toolbar style="padding: 8px;">
|
||||||
<ng-container *ngIf="!($error$ | async)">
|
<ng-container *ngIf="!($initializing$ | async) && !($error$ | async) && { loading: currentSlide.$loading$ | async, bar: currentBottomBar} as v">
|
||||||
<ion-button slot="start" *ngIf="($anythingLoading$ | async) && currentSlideDef.cancelButton.whileLoading as cancel" (click)="finished({ cancelled: true })" class="toolbar-button" fill="outline" color="medium">
|
|
||||||
|
<!-- cancel button if loading/not loading -->
|
||||||
|
<ion-button slot="start" *ngIf="v.loading && v.bar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
|
||||||
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
||||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||||
</ion-button>
|
</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-button slot="start" *ngIf="!v.loading && v.bar.cancel.afterLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
|
||||||
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
||||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.nextButton as nextButton" (click)="finished({})" 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="!($anythingLoading$ | async) && currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
|
<!-- next button -->
|
||||||
|
<ion-button slot="end" *ngIf="!v.loading && v.bar.next as next" (click)="transitions.next()" fill="outline" class="toolbar-button" color="primary">
|
||||||
|
<ion-text [class.smaller-text]="next.length > 16">{{next}}</ion-text>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
|
<!-- finish button -->
|
||||||
|
<ion-button slot="end" *ngIf="!v.loading && v.bar.finish as finish" (click)="transitions.final()" fill="outline" class="toolbar-button" color="primary">
|
||||||
|
<ion-text [class.smaller-text]="finish.length > 16">{{finish}}</ion-text>
|
||||||
|
</ion-button>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="$error$ | async">
|
<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>
|
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-footer>
|
</ion-footer>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||||
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
||||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
|
import { BehaviorSubject, from, Observable, of } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
import { Cleanup } from 'src/app/util/cleanup'
|
||||||
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
|
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
|
||||||
import { CompleteComponent } from './complete/complete.component'
|
import { CompleteComponent } from './complete/complete.component'
|
||||||
import { DependenciesComponent } from './dependencies/dependencies.component'
|
import { DependenciesComponent } from './dependencies/dependencies.component'
|
||||||
import { DependentsComponent } from './dependents/dependents.component'
|
import { DependentsComponent } from './dependents/dependents.component'
|
||||||
import { NotesComponent } from './notes/notes.component'
|
import { NotesComponent } from './notes/notes.component'
|
||||||
import { Colorable, Loadable } from './loadable'
|
import { Loadable } from './loadable'
|
||||||
import { WizardAction } from './wizard-types'
|
import { WizardAction } from './wizard-types'
|
||||||
|
import { concatMap, switchMap } from 'rxjs/operators'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'install-wizard',
|
selector: 'install-wizard',
|
||||||
@@ -18,36 +18,35 @@ import { WizardAction } from './wizard-types'
|
|||||||
})
|
})
|
||||||
export class InstallWizardComponent extends Cleanup implements OnInit {
|
export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||||
@Input() params: {
|
@Input() params: {
|
||||||
// defines the slideshow in the html
|
// defines each slide along with bottom bar
|
||||||
slideDefinitions: SlideDefinition[]
|
slideDefinitions: SlideDefinition[]
|
||||||
toolbar: TopbarParams
|
toolbar: TopbarParams
|
||||||
}
|
}
|
||||||
|
|
||||||
// containers
|
// content container so we can scroll to top between slide transitions
|
||||||
@ViewChild(IonContent) contentContainer: IonContent
|
@ViewChild(IonContent) contentContainer: IonContent
|
||||||
|
// slide container gives us hook into transitioning slides
|
||||||
@ViewChild(IonSlides) slideContainer: IonSlides
|
@ViewChild(IonSlides) slideContainer: IonSlides
|
||||||
|
|
||||||
//don't use this, use slideComponents instead.
|
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
|
||||||
@ViewChildren('components')
|
@ViewChildren('components')
|
||||||
public slideComponentsQL: QueryList<Loadable & Colorable>
|
slideComponentsQL: QueryList<Loadable>
|
||||||
|
get slideComponents (): Loadable[] { return this.slideComponentsQL.toArray() }
|
||||||
|
|
||||||
//don't use this, use currentSlide instead.
|
private slideIndex = 0
|
||||||
slideIndex = 0
|
get currentSlide (): Loadable {
|
||||||
|
|
||||||
get slideComponents (): (Loadable & Colorable)[] {
|
|
||||||
return this.slideComponentsQL.toArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentSlide (): (Loadable & Colorable) {
|
|
||||||
return this.slideComponents[this.slideIndex]
|
return this.slideComponents[this.slideIndex]
|
||||||
}
|
}
|
||||||
|
get currentSlideLoading$ (): Observable<boolean> {
|
||||||
get currentSlideDef (): SlideDefinition {
|
return this.$initializing$.pipe(switchMap(
|
||||||
return this.params.slideDefinitions[this.slideIndex]
|
i => i ? of(true) : this.currentSlide.$loading$,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
get currentBottomBar (): SlideDefinition['bottomBar'] {
|
||||||
|
return this.params.slideDefinitions[this.slideIndex].bottomBar
|
||||||
}
|
}
|
||||||
|
|
||||||
$anythingLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true)
|
$initializing$ = new BehaviorSubject(true)
|
||||||
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
|
|
||||||
$error$ = new BehaviorSubject(undefined)
|
$error$ = new BehaviorSubject(undefined)
|
||||||
|
|
||||||
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
|
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
|
||||||
@@ -60,26 +59,28 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ionViewDidEnter () {
|
ionViewDidEnter () {
|
||||||
this.cleanup(
|
this.$initializing$.next(false)
|
||||||
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 }) => {
|
// process bottom bar buttons
|
||||||
|
private transition = (info: { error?: Error, cancelled?: true, final?: true }) => {
|
||||||
if (info.cancelled) this.currentSlide.$cancel$.next()
|
if (info.cancelled) this.currentSlide.$cancel$.next()
|
||||||
if (info.final || info.cancelled) return this.modalController.dismiss(info)
|
if (info.final || info.cancelled) return this.modalController.dismiss(info)
|
||||||
if (info.error) return this.$error$.next(capitalizeFirstLetter(info.error.message))
|
if (info.error) return this.$error$.next(capitalizeFirstLetter(info.error.message))
|
||||||
|
|
||||||
this.slide()
|
this.moveToNextSlide()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async slide () {
|
// bottom bar button callbacks
|
||||||
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
|
transitions = {
|
||||||
|
cancel: () => this.transition({ cancelled: true }),
|
||||||
|
next: () => this.transition({ }),
|
||||||
|
final: () => this.transition({ final: true }),
|
||||||
|
error: e => this.transition({ error: e }),
|
||||||
|
}
|
||||||
|
|
||||||
|
private async moveToNextSlide () {
|
||||||
|
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.transition({ final: true }) }
|
||||||
this.zone.run(async () => {
|
this.zone.run(async () => {
|
||||||
this.slideComponents[this.slideIndex + 1].load()
|
this.slideComponents[this.slideIndex + 1].load()
|
||||||
await pauseFor(50)
|
await pauseFor(50)
|
||||||
@@ -92,33 +93,23 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlideCommon {
|
export interface SlideDefinition {
|
||||||
selector: string // component http selector
|
slide:
|
||||||
cancelButton: {
|
{ selector: 'dependencies', params: DependenciesComponent['params'] } |
|
||||||
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
|
{ selector: 'dependents', params: DependentsComponent['params'] } |
|
||||||
afterLoading?: { text?: string },
|
{ selector: 'complete', params: CompleteComponent['params'] } |
|
||||||
whileLoading?: { text?: string }
|
{ selector: 'notes', params: NotesComponent['params'] }
|
||||||
|
bottomBar: {
|
||||||
|
cancel: {
|
||||||
|
// indicates the existence of a cancel button, and whether to have text or an icon 'x' by default.
|
||||||
|
afterLoading?: { text?: string },
|
||||||
|
whileLoading?: { text?: string }
|
||||||
|
}
|
||||||
|
next?: string
|
||||||
|
finish?: string
|
||||||
}
|
}
|
||||||
nextButton?: string, // existence and content of next button
|
|
||||||
finishButton?: string // existence and content of finish button
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SlideDefinition = SlideCommon & (
|
|
||||||
{
|
|
||||||
selector: 'dependencies',
|
|
||||||
params: DependenciesComponent['params']
|
|
||||||
} | {
|
|
||||||
selector: 'dependents',
|
|
||||||
params: DependentsComponent['params']
|
|
||||||
} | {
|
|
||||||
selector: 'complete',
|
|
||||||
params: CompleteComponent['params']
|
|
||||||
} | {
|
|
||||||
selector: 'notes',
|
|
||||||
params: NotesComponent['params']
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export type TopbarParams = { action: WizardAction, title: string, version: string }
|
export type TopbarParams = { action: WizardAction, title: string, version: string }
|
||||||
|
|
||||||
export async function wizardModal (
|
export async function wizardModal (
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { BehaviorSubject, Subject } from 'rxjs'
|
import { BehaviorSubject, Subject } from 'rxjs'
|
||||||
|
|
||||||
export interface Loadable {
|
export interface Loadable {
|
||||||
load: () => void
|
load: (prevResult?: any) => void
|
||||||
$loading$: BehaviorSubject<boolean> //will be true during load function
|
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.
|
||||||
$cancel$: Subject<void> //will cancel load function
|
$loading$: BehaviorSubject<boolean> // will be true during load function
|
||||||
|
$cancel$: Subject<void> // will cancel load function
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Colorable {
|
|
||||||
$color$: BehaviorSubject<string>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="slide-content">
|
<div class="slide-content">
|
||||||
<div style="margin-top: 25px;">
|
<div style="margin-top: 25px;">
|
||||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||||
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
|
<ion-label [color]="params.titleColor" style="font-size: xx-large; font-weight: bold;">
|
||||||
{{params.title}}
|
{{params.title}}
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input, OnInit } from '@angular/core'
|
||||||
import { BehaviorSubject, Subject } from 'rxjs'
|
import { BehaviorSubject, Subject } from 'rxjs'
|
||||||
import { Colorable, Loadable } from '../loadable'
|
import { Loadable } from '../loadable'
|
||||||
import { WizardAction } from '../wizard-types'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'notes',
|
selector: 'notes',
|
||||||
templateUrl: './notes.component.html',
|
templateUrl: './notes.component.html',
|
||||||
styleUrls: ['../install-wizard.component.scss'],
|
styleUrls: ['../install-wizard.component.scss'],
|
||||||
})
|
})
|
||||||
export class NotesComponent implements OnInit, Loadable, Colorable {
|
export class NotesComponent implements OnInit, Loadable {
|
||||||
@Input() params: {
|
@Input() params: {
|
||||||
action: WizardAction
|
|
||||||
notes: string
|
notes: string
|
||||||
title: string
|
title: string
|
||||||
titleColor: string
|
titleColor: string
|
||||||
}
|
}
|
||||||
|
|
||||||
$loading$ = new BehaviorSubject(false)
|
$loading$ = new BehaviorSubject(false)
|
||||||
$color$ = new BehaviorSubject('light')
|
|
||||||
$cancel$ = new Subject<void>()
|
$cancel$ = new Subject<void>()
|
||||||
|
|
||||||
load () { }
|
load () { }
|
||||||
|
|
||||||
constructor () { }
|
constructor () { }
|
||||||
ngOnInit () { this.$color$.next(this.params.titleColor) }
|
ngOnInit () { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { exists } from 'src/app/util/misc.util'
|
|||||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
||||||
import { ApiService } from '../../services/api/api.service'
|
import { ApiService } from '../../services/api/api.service'
|
||||||
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
|
||||||
|
import { WizardAction } from './wizard-types'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class WizardBaker {
|
export class WizardBaker {
|
||||||
constructor (
|
constructor (
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly updateService: OsUpdateService,
|
private readonly updateService: OsUpdateService,
|
||||||
private readonly appModel: AppModel
|
private readonly appModel: AppModel,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
install (values: {
|
install (values: {
|
||||||
@@ -28,17 +29,40 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
installAlert ? {
|
||||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
slide: {
|
||||||
}} : undefined,
|
selector: 'notes',
|
||||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
|
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
|
||||||
action, title, version, serviceRequirements,
|
},
|
||||||
}},
|
bottomBar: {
|
||||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next',
|
||||||
action, verb: 'beginning installation for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
},
|
||||||
this.appModel.add({ ...app, status: AppStatus.INSTALLING })
|
} : undefined,
|
||||||
}),
|
{
|
||||||
}},
|
slide: {
|
||||||
|
selector: 'dependencies',
|
||||||
|
params: { action, title, version, serviceRequirements },
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { afterLoading: { text: 'Cancel' } },
|
||||||
|
next: 'Install',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slide: {
|
||||||
|
selector: 'complete',
|
||||||
|
params: {
|
||||||
|
action,
|
||||||
|
verb: 'beginning installation for',
|
||||||
|
title,
|
||||||
|
executeAction: () => this.apiService.installApp(id, version).then(app => { this.appModel.add({ ...app, status: AppStatus.INSTALLING })}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { whileLoading: { } },
|
||||||
|
finish: 'Dismiss',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||||
}
|
}
|
||||||
@@ -57,20 +81,49 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
installAlert ? {
|
||||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
slide: {
|
||||||
}} : undefined,
|
selector: 'notes',
|
||||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
|
params: { notes: installAlert, title: 'Warning', titleColor: 'warning'},
|
||||||
action, title, version, serviceRequirements,
|
},
|
||||||
}},
|
bottomBar: {
|
||||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Update Anyways', params: {
|
cancel: { afterLoading: { text: 'Cancel' } },
|
||||||
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
next: 'Next',
|
||||||
}},
|
},
|
||||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
} : undefined,
|
||||||
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
{ slide: {
|
||||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
selector: 'dependencies',
|
||||||
}),
|
params: { action, title, version, serviceRequirements },
|
||||||
}},
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { afterLoading: { text: 'Cancel' } },
|
||||||
|
next: 'Update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ slide: {
|
||||||
|
selector: 'dependents',
|
||||||
|
params: {
|
||||||
|
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { afterLoading: { text: 'Cancel' } },
|
||||||
|
next: 'Update Anyways',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ 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 })
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { whileLoading: { } },
|
||||||
|
finish: 'Dismiss',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||||
}
|
}
|
||||||
@@ -85,12 +138,25 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update OS', params: {
|
{ slide : {
|
||||||
action, notes: releaseNotes || 'No release notes for this version', title: 'Release Notes', titleColor: 'dark',
|
selector: 'notes',
|
||||||
}},
|
params: { notes: releaseNotes, title: 'Release Notes', titleColor: 'dark' },
|
||||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
},
|
||||||
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
|
bottomBar: {
|
||||||
}},
|
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ slide: {
|
||||||
|
selector: 'complete',
|
||||||
|
params: {
|
||||||
|
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { whileLoading: { }},
|
||||||
|
finish: 'Dismiss',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||||
}
|
}
|
||||||
@@ -109,20 +175,45 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
installAlert ? {
|
||||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
slide: {
|
||||||
}} : undefined,
|
selector: 'notes',
|
||||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
|
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
|
||||||
action, title, version, serviceRequirements,
|
},
|
||||||
}},
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next' },
|
||||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade Anyways', params: {
|
} : undefined,
|
||||||
skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
|
{ slide: {
|
||||||
}},
|
selector: 'dependencies',
|
||||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
params: { action, title, version, serviceRequirements },
|
||||||
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
|
},
|
||||||
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
|
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 ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Downgrade Anyways',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ 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 })
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
cancel: { whileLoading: { } },
|
||||||
|
finish: 'Dismiss',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||||
}
|
}
|
||||||
@@ -136,19 +227,36 @@ export class WizardBaker {
|
|||||||
validate(title, exists, 'missing title')
|
validate(title, exists, 'missing title')
|
||||||
validate(version, exists, 'missing version')
|
validate(version, exists, 'missing version')
|
||||||
|
|
||||||
const action = 'uninstall'
|
const action = 'uninstall' as WizardAction
|
||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
|
{ slide: {
|
||||||
action, notes: uninstallAlert || defaultUninstallationWarning(title), title: 'Warning', titleColor: 'warning' },
|
selector: 'notes',
|
||||||
|
params: {
|
||||||
|
notes: uninstallAlert || defaultUninstallationWarning(title),
|
||||||
|
title: 'Warning',
|
||||||
|
titleColor: 'warning',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Continue' },
|
||||||
|
},
|
||||||
|
{ slide: {
|
||||||
|
selector: 'dependents',
|
||||||
|
params: {
|
||||||
|
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' },
|
||||||
|
},
|
||||||
|
{ slide: {
|
||||||
|
selector: 'complete',
|
||||||
|
params: {
|
||||||
|
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },
|
||||||
},
|
},
|
||||||
{ 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: slideDefinitions.filter(exists) }
|
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||||
}
|
}
|
||||||
@@ -166,9 +274,14 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Stop Anyways', params: {
|
{ slide: {
|
||||||
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
|
selector: 'dependents',
|
||||||
}},
|
params: {
|
||||||
|
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Stop Anyways' },
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return { toolbar, slideDefinitions }
|
return { toolbar, slideDefinitions }
|
||||||
}
|
}
|
||||||
@@ -182,9 +295,14 @@ export class WizardBaker {
|
|||||||
const toolbar: TopbarParams = { action, title, version }
|
const toolbar: TopbarParams = { action, title, version }
|
||||||
|
|
||||||
const slideDefinitions: SlideDefinition[] = [
|
const slideDefinitions: SlideDefinition[] = [
|
||||||
{ selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Save Config Anyways', params: {
|
{ slide: {
|
||||||
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
|
selector: 'dependents',
|
||||||
}},
|
params: {
|
||||||
|
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Save Config Anyways' },
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return { toolbar, slideDefinitions }
|
return { toolbar, slideDefinitions }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Cleanup } from 'src/app/util/cleanup'
|
|||||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||||
import { Emver } from 'src/app/services/emver.service'
|
import { Emver } from 'src/app/services/emver.service'
|
||||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||||
|
import { pauseFor } from 'src/app/util/misc.util'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-available-show',
|
selector: 'app-available-show',
|
||||||
@@ -160,6 +161,7 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
await pauseFor(250)
|
||||||
this.navCtrl.back()
|
this.navCtrl.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,18 +176,16 @@ export class AppAvailableShowPage extends Cleanup {
|
|||||||
installAlert: app.installAlert,
|
installAlert: app.installAlert,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (action) {
|
const { cancelled } = await wizardModal(
|
||||||
case 'update':
|
this.modalCtrl,
|
||||||
return wizardModal(
|
action === 'update' ?
|
||||||
this.modalCtrl,
|
this.wizardBaker.update(value) :
|
||||||
this.wizardBaker.update(value),
|
this.wizardBaker.downgrade(value),
|
||||||
).then(({ cancelled }) => cancelled || this.navCtrl.back())
|
)
|
||||||
case 'downgrade':
|
|
||||||
return wizardModal(
|
if (cancelled) return
|
||||||
this.modalCtrl,
|
await pauseFor(250)
|
||||||
this.wizardBaker.downgrade(value),
|
this.navCtrl.back()
|
||||||
).then(({ cancelled }) => cancelled || this.navCtrl.back())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchRecommendation (): Observable<any> {
|
private fetchRecommendation (): Observable<any> {
|
||||||
|
|||||||
Reference in New Issue
Block a user