mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
refactor app wizards completely (#1537)
* refactor app wizards completely * display new and new options in config Co-authored-by: Matt Hill <matthill@Matt-M1.start9.dev>
This commit is contained in:
59
frontend/package-lock.json
generated
59
frontend/package-lock.json
generated
@@ -37,6 +37,7 @@
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^6.6.7",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.1.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
@@ -5879,6 +5880,14 @@
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom7": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz",
|
||||
"integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==",
|
||||
"dependencies": {
|
||||
"ssr-window": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
@@ -12805,6 +12814,11 @@
|
||||
"integrity": "sha512-ZPO9rECxzs5JIQ6G/2EfL1I9ho/BVZkx9HRKn8+0af7QgwAmumQ7XBFP1ggMyPMo+/tUbmv0HFdv4qifdO/9JA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ssr-window": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
|
||||
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
|
||||
},
|
||||
"node_modules/ssri": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||
@@ -13052,6 +13066,29 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swiper": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.2.4.tgz",
|
||||
"integrity": "sha512-TPq64KiZUt8lZY5ZEg75RjToT+RwfLomfKIpcFLy6+UCUp2kL7hHWslLxjFtcFeiwfG67RHFYbJnq6tsothcJQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/swiperjs"
|
||||
},
|
||||
{
|
||||
"type": "open_collective",
|
||||
"url": "http://opencollective.com/swiper"
|
||||
}
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"dom7": "^4.0.4",
|
||||
"ssr-window": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
@@ -18631,6 +18668,14 @@
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dom7": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz",
|
||||
"integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==",
|
||||
"requires": {
|
||||
"ssr-window": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
@@ -23758,6 +23803,11 @@
|
||||
"integrity": "sha512-ZPO9rECxzs5JIQ6G/2EfL1I9ho/BVZkx9HRKn8+0af7QgwAmumQ7XBFP1ggMyPMo+/tUbmv0HFdv4qifdO/9JA==",
|
||||
"dev": true
|
||||
},
|
||||
"ssr-window": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
|
||||
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
|
||||
},
|
||||
"ssri": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
|
||||
@@ -23939,6 +23989,15 @@
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true
|
||||
},
|
||||
"swiper": {
|
||||
"version": "8.2.4",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.2.4.tgz",
|
||||
"integrity": "sha512-TPq64KiZUt8lZY5ZEg75RjToT+RwfLomfKIpcFLy6+UCUp2kL7hHWslLxjFtcFeiwfG67RHFYbJnq6tsothcJQ==",
|
||||
"requires": {
|
||||
"dom7": "^4.0.4",
|
||||
"ssr-window": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^6.6.7",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.1.0",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface MarketplaceManifest<T = unknown> {
|
||||
uninstall: string | null
|
||||
restore: string | null
|
||||
start: string | null
|
||||
stop: string | null
|
||||
}
|
||||
dependencies: Record<string, Dependency<T>>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
<!-- 3rd party components -->
|
||||
<qr-code value="hello"></qr-code>
|
||||
<swiper>
|
||||
<ng-template swiperSlide>Slide 1</ng-template>
|
||||
</swiper>
|
||||
|
||||
<!-- Ionic components -->
|
||||
<ion-action-sheet></ion-action-sheet>
|
||||
@@ -46,7 +49,6 @@
|
||||
<ion-segment-button></ion-segment-button>
|
||||
<ion-select></ion-select>
|
||||
<ion-select-option></ion-select-option>
|
||||
<ion-slides></ion-slides>
|
||||
<ion-spinner name="lines"></ion-spinner>
|
||||
<ion-text></ion-text>
|
||||
<ion-text><strong>load bold font</strong></ion-text>
|
||||
|
||||
@@ -2,11 +2,11 @@ import { CommonModule } from '@angular/common'
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
import { PreloaderComponent } from './preloader.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, QrCodeModule],
|
||||
imports: [CommonModule, IonicModule, QrCodeModule, SwiperModule],
|
||||
declarations: [PreloaderComponent],
|
||||
exports: [PreloaderComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<h1>
|
||||
<ion-text color="warning">Warning</ion-text>
|
||||
</h1>
|
||||
<div class="ion-text-left" [innerHTML]="params.message | markdown"></div>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BaseSlide } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'alert',
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrls: ['../app-wizard.component.scss'],
|
||||
})
|
||||
export class AlertComponent implements BaseSlide {
|
||||
@Input() params: {
|
||||
message: string
|
||||
}
|
||||
|
||||
async load() {}
|
||||
|
||||
loading = false
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<div style="padding: 10px 0">
|
||||
<ion-title style="font-size: 32px">{{ params.title }}</ion-title>
|
||||
<div class="underline"></div>
|
||||
<ion-title>
|
||||
<i
|
||||
>{{ params.action | titlecase
|
||||
}}<span *ngIf="params.version"
|
||||
>: {{ params.version | displayEmver }}</span
|
||||
></i
|
||||
>
|
||||
</ion-title>
|
||||
</div>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<div style="padding: 36px; height: 100%">
|
||||
<swiper
|
||||
*ngIf="!error; else hasError"
|
||||
(swiper)="setSwiperInstance($event)"
|
||||
(slideNextTransitionStart)="loadSlide()"
|
||||
>
|
||||
<ng-template swiperSlide *ngFor="let slide of params.slides">
|
||||
<alert
|
||||
#components
|
||||
*ngIf="slide.selector === 'alert'"
|
||||
[params]="slide.params"
|
||||
></alert>
|
||||
<notes
|
||||
#components
|
||||
*ngIf="slide.selector === 'notes'"
|
||||
[params]="slide.params"
|
||||
></notes>
|
||||
<dependents
|
||||
#components
|
||||
*ngIf="slide.selector === 'dependents'"
|
||||
[params]="slide.params"
|
||||
(onSuccess)="next()"
|
||||
(onError)="setError($event)"
|
||||
></dependents>
|
||||
<complete
|
||||
#components
|
||||
*ngIf="slide.selector === 'complete'"
|
||||
[params]="slide.params"
|
||||
(onSuccess)="dismiss('success')"
|
||||
(onError)="setError($event)"
|
||||
></complete>
|
||||
</ng-template>
|
||||
</swiper>
|
||||
|
||||
<ng-template #hasError>
|
||||
<p>
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ng-container *ngIf="!initializing && swiper">
|
||||
<ion-buttons slot="end" style="padding-right: 8px">
|
||||
<ion-button
|
||||
*ngIf="error; else noError"
|
||||
(click)="dismiss()"
|
||||
class="enter-click"
|
||||
>
|
||||
<b>Dismiss</b>
|
||||
</ion-button>
|
||||
<ng-template #noError>
|
||||
<ion-button
|
||||
*ngIf="!currentSlide.loading && !swiper.isEnd"
|
||||
(click)="next()"
|
||||
class="enter-click"
|
||||
[class.no-click]="currentSlide.loading"
|
||||
>
|
||||
<b>Continue</b>
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</ion-buttons>
|
||||
</ng-container>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { InstallWizardComponent } from './install-wizard.component'
|
||||
import { AppWizardComponent } from './app-wizard.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { EmverPipesModule } from '@start9labs/shared'
|
||||
@@ -8,9 +8,10 @@ import { DependentsComponentModule } from './dependents/dependents.component.mod
|
||||
import { CompleteComponentModule } from './complete/complete.component.module'
|
||||
import { NotesComponentModule } from './notes/notes.component.module'
|
||||
import { AlertComponentModule } from './alert/alert.component.module'
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
|
||||
@NgModule({
|
||||
declarations: [InstallWizardComponent],
|
||||
declarations: [AppWizardComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
@@ -20,7 +21,8 @@ import { AlertComponentModule } from './alert/alert.component.module'
|
||||
CompleteComponentModule,
|
||||
NotesComponentModule,
|
||||
AlertComponentModule,
|
||||
SwiperModule,
|
||||
],
|
||||
exports: [InstallWizardComponent],
|
||||
exports: [AppWizardComponent],
|
||||
})
|
||||
export class InstallWizardComponentModule {}
|
||||
export class AppWizardComponentModule {}
|
||||
@@ -0,0 +1,6 @@
|
||||
.underline {
|
||||
margin: 6px 0 8px 16px;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #404040;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { IonContent, ModalController } from '@ionic/angular'
|
||||
import { CompleteComponent } from './complete/complete.component'
|
||||
import { DependentsComponent } from './dependents/dependents.component'
|
||||
import { AlertComponent } from './alert/alert.component'
|
||||
import { NotesComponent } from './notes/notes.component'
|
||||
import { WizardAction } from './wizard-types'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { IonicSlides } from '@ionic/angular'
|
||||
import { BaseSlide } from './wizard-types'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
|
||||
@Component({
|
||||
selector: 'app-wizard',
|
||||
templateUrl: './app-wizard.component.html',
|
||||
styleUrls: ['./app-wizard.component.scss'],
|
||||
})
|
||||
export class AppWizardComponent {
|
||||
@Input() params: {
|
||||
action: WizardAction
|
||||
title: string
|
||||
slides: SlideDefinition[]
|
||||
version?: string
|
||||
}
|
||||
|
||||
// content container so we can scroll to top between slide transitions
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
swiper: Swiper
|
||||
|
||||
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
|
||||
@ViewChildren('components')
|
||||
slideComponentsQL: QueryList<BaseSlide>
|
||||
|
||||
get slideComponents(): BaseSlide[] {
|
||||
return this.slideComponentsQL.toArray()
|
||||
}
|
||||
|
||||
get currentSlide(): BaseSlide {
|
||||
return this.slideComponents[this.currentIndex]
|
||||
}
|
||||
|
||||
get currentIndex(): number {
|
||||
return this.swiper.activeIndex
|
||||
}
|
||||
|
||||
initializing = true
|
||||
error = ''
|
||||
|
||||
constructor(private readonly modalController: ModalController) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
this.initializing = false
|
||||
this.swiper.allowTouchMove = false
|
||||
this.loadSlide()
|
||||
}
|
||||
|
||||
setSwiperInstance(swiper: any) {
|
||||
this.swiper = swiper
|
||||
}
|
||||
|
||||
dismiss(role = 'cancelled') {
|
||||
this.modalController.dismiss(null, role)
|
||||
}
|
||||
|
||||
async next() {
|
||||
await this.content.scrollToTop()
|
||||
this.swiper.slideNext(500)
|
||||
}
|
||||
|
||||
setError(e: any) {
|
||||
console.log(e)
|
||||
this.error = e
|
||||
}
|
||||
|
||||
async loadSlide() {
|
||||
this.currentSlide.load()
|
||||
}
|
||||
}
|
||||
|
||||
export type SlideDefinition =
|
||||
| { selector: 'alert'; params: AlertComponent['params'] }
|
||||
| { selector: 'notes'; params: NotesComponent['params'] }
|
||||
| { selector: 'dependents'; params: DependentsComponent['params'] }
|
||||
| { selector: 'complete'; params: CompleteComponent['params'] }
|
||||
|
||||
export async function wizardModal(
|
||||
modalController: ModalController,
|
||||
params: AppWizardComponent['params'],
|
||||
): Promise<boolean> {
|
||||
const modal = await modalController.create({
|
||||
backdropDismiss: false,
|
||||
cssClass: 'wizard-modal',
|
||||
component: AppWizardComponent,
|
||||
componentProps: { params },
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
return modal.onDidDismiss().then(({ role }) => role === 'success')
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<div style="padding: 32px">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
@@ -5,14 +5,8 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CompleteComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild([]),
|
||||
],
|
||||
declarations: [CompleteComponent],
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild([])],
|
||||
exports: [CompleteComponent],
|
||||
})
|
||||
export class CompleteComponentModule { }
|
||||
export class CompleteComponentModule {}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { capitalizeFirstLetter } from '@start9labs/shared'
|
||||
import { BaseSlide } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'complete',
|
||||
templateUrl: './complete.component.html',
|
||||
styleUrls: ['../app-wizard.component.scss'],
|
||||
})
|
||||
export class CompleteComponent implements BaseSlide {
|
||||
@Input() params: {
|
||||
verb: string // loader verb: '*stopping* ...'
|
||||
title: string
|
||||
Fn: () => Promise<any>
|
||||
}
|
||||
|
||||
@Output() onSuccess: EventEmitter<void> = new EventEmitter()
|
||||
@Output() onError: EventEmitter<string> = new EventEmitter()
|
||||
|
||||
message: string
|
||||
|
||||
loading = true
|
||||
|
||||
async load() {
|
||||
this.message =
|
||||
capitalizeFirstLetter(this.params.verb) + ' ' + this.params.title
|
||||
try {
|
||||
await this.params.Fn()
|
||||
this.onSuccess.emit()
|
||||
} catch (e: any) {
|
||||
this.onError.emit(`Error: ${e.message || e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<div *ngIf="loading; else loaded" style="padding: 32px">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<p>Checking for installed services which depend on {{ params.title }}...</p>
|
||||
</div>
|
||||
|
||||
<ng-template #loaded>
|
||||
<h1><ion-text color="warning">Warning</ion-text></h1>
|
||||
<p>{{ warningMessage }}</p>
|
||||
|
||||
<ng-container *ngIf="pkgs$ | async as pkgs">
|
||||
<ion-item-group>
|
||||
<ion-item-divider class="ion-padding-bottom">
|
||||
Affected Services
|
||||
</ion-item-divider>
|
||||
<ion-item *ngFor="let dep of breakages | keyvalue">
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="pkgs[dep.key]['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
{{ pkgs[dep.key].manifest.title }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { capitalizeFirstLetter, isEmptyObject } from '@start9labs/shared'
|
||||
import { BaseSlide } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'dependents',
|
||||
templateUrl: './dependents.component.html',
|
||||
styleUrls: ['./dependents.component.scss', '../app-wizard.component.scss'],
|
||||
})
|
||||
export class DependentsComponent implements BaseSlide {
|
||||
@Input() params: {
|
||||
title: string
|
||||
verb: string // *Uninstalling* will cause problems...
|
||||
Fn: () => Promise<Breakages>
|
||||
}
|
||||
|
||||
@Output() onSuccess: EventEmitter<void> = new EventEmitter()
|
||||
@Output() onError: EventEmitter<string> = new EventEmitter()
|
||||
|
||||
breakages: Breakages
|
||||
warningMessage: string | undefined
|
||||
|
||||
loading = true
|
||||
|
||||
readonly pkgs$ = this.patch.watch$('package-data')
|
||||
|
||||
constructor(public readonly patch: PatchDbService) {}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
this.breakages = await this.params.Fn()
|
||||
if (this.breakages && !isEmptyObject(this.breakages)) {
|
||||
this.warningMessage =
|
||||
capitalizeFirstLetter(this.params.verb) +
|
||||
' ' +
|
||||
this.params.title +
|
||||
' will prohibit the following services from functioning properly.'
|
||||
} else {
|
||||
this.onSuccess.emit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.onError.emit(
|
||||
`Error fetching dependent service information: ${e.message || e}`,
|
||||
)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="ion-text-left">
|
||||
<h1>Release Notes</h1>
|
||||
<br />
|
||||
<div *ngFor="let v of params.versions">
|
||||
<h4>
|
||||
<b>
|
||||
{{ v.version }}
|
||||
</b>
|
||||
</h4>
|
||||
<hr style="height: 0; border-width: 1px" />
|
||||
<div [innerHTML]="v.notes | markdown"></div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,22 +1,20 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BehaviorSubject, Subject } from 'rxjs'
|
||||
import { BaseSlide } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'notes',
|
||||
templateUrl: './notes.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
styleUrls: ['../app-wizard.component.scss'],
|
||||
})
|
||||
export class NotesComponent {
|
||||
export class NotesComponent implements BaseSlide {
|
||||
@Input() params: {
|
||||
versions: { version: string; notes: string }[]
|
||||
title: string
|
||||
titleColor: string
|
||||
headline: string
|
||||
}
|
||||
|
||||
load() {}
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
loading = false
|
||||
|
||||
async load() {}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
@@ -0,0 +1,269 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { Manifest } from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from '../../services/api/embassy-api.service'
|
||||
import { AppWizardComponent, SlideDefinition } from './app-wizard.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { first } from 'rxjs/operators'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WizardDefs {
|
||||
constructor(
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly config: ConfigService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
update(values: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
installAlert?: string
|
||||
}): AppWizardComponent['params'] {
|
||||
const { id, title, version, installAlert } = values
|
||||
|
||||
const slides: Array<SlideDefinition | undefined> = [
|
||||
installAlert
|
||||
? {
|
||||
selector: 'alert',
|
||||
params: {
|
||||
message: installAlert,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
verb: 'updating',
|
||||
title,
|
||||
Fn: () => this.embassyApi.dryUpdatePackage({ id, version }),
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'complete',
|
||||
params: {
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
Fn: () =>
|
||||
this.marketplaceService
|
||||
.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
.pipe(first())
|
||||
.toPromise(),
|
||||
},
|
||||
},
|
||||
]
|
||||
return {
|
||||
action: 'update',
|
||||
title,
|
||||
version,
|
||||
slides: slides.filter(exists),
|
||||
}
|
||||
}
|
||||
|
||||
updateOS(values: {
|
||||
version: string
|
||||
releaseNotes: { [version: string]: string }
|
||||
headline: string
|
||||
}): AppWizardComponent['params'] {
|
||||
const { version, releaseNotes, headline } = values
|
||||
|
||||
const versions = Object.keys(releaseNotes)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(version => {
|
||||
return {
|
||||
version,
|
||||
notes: releaseNotes[version],
|
||||
}
|
||||
})
|
||||
|
||||
const title = 'EmbassyOS'
|
||||
|
||||
const slides: SlideDefinition[] = [
|
||||
{
|
||||
selector: 'notes',
|
||||
params: {
|
||||
versions,
|
||||
headline,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'complete',
|
||||
params: {
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
Fn: () =>
|
||||
this.embassyApi.updateServer({
|
||||
'marketplace-url': this.config.marketplace.url,
|
||||
}),
|
||||
},
|
||||
},
|
||||
]
|
||||
return {
|
||||
action: 'update',
|
||||
title,
|
||||
version,
|
||||
slides: slides.filter(exists),
|
||||
}
|
||||
}
|
||||
|
||||
downgrade(values: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
installAlert?: string
|
||||
}): AppWizardComponent['params'] {
|
||||
const { id, title, version, installAlert } = values
|
||||
|
||||
const slides: Array<SlideDefinition | undefined> = [
|
||||
installAlert
|
||||
? {
|
||||
selector: 'alert',
|
||||
params: {
|
||||
message: installAlert,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
verb: 'downgrading',
|
||||
title,
|
||||
Fn: () => this.embassyApi.dryUpdatePackage({ id, version }),
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'complete',
|
||||
params: {
|
||||
verb: 'beginning downgrade for',
|
||||
title,
|
||||
Fn: () =>
|
||||
this.marketplaceService
|
||||
.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
.pipe(first())
|
||||
.toPromise(),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
action: 'downgrade',
|
||||
title,
|
||||
version,
|
||||
slides: slides.filter(exists),
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(values: {
|
||||
id: string
|
||||
title: string
|
||||
uninstallAlert?: string
|
||||
}): AppWizardComponent['params'] {
|
||||
const { id, title, uninstallAlert } = values
|
||||
|
||||
const slides: SlideDefinition[] = [
|
||||
{
|
||||
selector: 'alert',
|
||||
params: {
|
||||
message: uninstallAlert || defaultUninstallWarning(title),
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
verb: 'uninstalling',
|
||||
title,
|
||||
Fn: () => this.embassyApi.dryUninstallPackage({ id }),
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'complete',
|
||||
params: {
|
||||
verb: 'uninstalling',
|
||||
title,
|
||||
Fn: () => this.embassyApi.uninstallPackage({ id }),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
action: 'uninstall',
|
||||
title,
|
||||
slides: slides.filter(exists),
|
||||
}
|
||||
}
|
||||
|
||||
stop(values: { id: string; title: string }): AppWizardComponent['params'] {
|
||||
const { title, id } = values
|
||||
|
||||
const slides: SlideDefinition[] = [
|
||||
{
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
verb: 'stopping',
|
||||
title,
|
||||
Fn: () => this.embassyApi.dryStopPackage({ id }),
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'complete',
|
||||
params: {
|
||||
verb: 'stopping',
|
||||
title,
|
||||
Fn: () => this.embassyApi.stopPackage({ id }),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
action: 'stop',
|
||||
title,
|
||||
slides: slides.filter(exists),
|
||||
}
|
||||
}
|
||||
|
||||
configure(values: {
|
||||
manifest: Manifest
|
||||
config: object
|
||||
}): AppWizardComponent['params'] {
|
||||
const { manifest, config } = values
|
||||
const { id, title } = manifest
|
||||
|
||||
const slides: SlideDefinition[] = [
|
||||
{
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
verb: 'saving config for',
|
||||
title,
|
||||
Fn: () => this.embassyApi.drySetPackageConfig({ id, config }),
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'complete',
|
||||
params: {
|
||||
verb: 'configuring',
|
||||
title,
|
||||
Fn: () => this.embassyApi.setPackageConfig({ id, config }),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
action: 'configure',
|
||||
title,
|
||||
slides: slides.filter(exists),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultUninstallWarning = (serviceName: string) =>
|
||||
`Uninstalling ${serviceName} will result in the deletion of its data.`
|
||||
@@ -0,0 +1,11 @@
|
||||
export type WizardAction =
|
||||
| 'update'
|
||||
| 'downgrade'
|
||||
| 'uninstall'
|
||||
| 'stop'
|
||||
| 'configure'
|
||||
|
||||
export interface BaseSlide {
|
||||
load: () => Promise<void>
|
||||
loading: boolean
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
<span>{{ data.spec.name }}</span>
|
||||
|
||||
<ion-text color="success" *ngIf="data.new"> (New)</ion-text>
|
||||
<ion-text color="success" *ngIf="data.newOptions"> (New Options)</ion-text>
|
||||
<ion-text color="warning" *ngIf="data.edited"> (Edited)</ion-text>
|
||||
|
||||
<span
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -116,9 +116,20 @@
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label
|
||||
><b>{{ spec.name }}</b></ion-label
|
||||
<ion-label>
|
||||
<b>
|
||||
{{ spec.name }}
|
||||
<ion-text *ngIf="entry.value.dirty" color="warning">
|
||||
(Edited)</ion-text
|
||||
>
|
||||
<ion-text
|
||||
*ngIf="original?.[entry.key] === undefined"
|
||||
color="success"
|
||||
>
|
||||
(New)</ion-text
|
||||
>
|
||||
</b>
|
||||
</ion-label>
|
||||
<!-- boolean -->
|
||||
<ion-toggle
|
||||
*ngIf="spec.type === 'boolean'"
|
||||
@@ -157,7 +168,8 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
new: original?.[entry.key] === undefined,
|
||||
newOptions: objectDisplay[entry.key].hasNewOptions,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -190,7 +202,8 @@
|
||||
: spec.spec
|
||||
"
|
||||
[formGroup]="$any(entry.value)"
|
||||
[current]="current ? current[entry.key] : undefined"
|
||||
[current]="current?.[entry.key]"
|
||||
[original]="original?.[entry.key]"
|
||||
[unionSpec]="spec.type === 'union' ? spec : undefined"
|
||||
(onExpand)="resize(entry.key)"
|
||||
></form-object>
|
||||
@@ -208,7 +221,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
@@ -282,6 +295,7 @@
|
||||
"
|
||||
[formGroup]="abstractControl"
|
||||
[current]="current?.[entry.key]?.[i]"
|
||||
[original]="original?.[entry.key]?.[i]"
|
||||
[unionSpec]="
|
||||
spec.subtype === 'union' ? $any(spec.spec) : undefined
|
||||
"
|
||||
@@ -347,7 +361,7 @@
|
||||
<form-label
|
||||
[data]="{
|
||||
spec: spec,
|
||||
new: !!current && current[entry.key] === undefined,
|
||||
new: original?.[entry.key] === undefined,
|
||||
edited: entry.value.dirty
|
||||
}"
|
||||
></form-label>
|
||||
|
||||
@@ -36,12 +36,14 @@ export class FormObjectComponent {
|
||||
@Input() formGroup: FormGroup
|
||||
@Input() unionSpec?: ValueSpecUnion
|
||||
@Input() current?: { [key: string]: any }
|
||||
@Input() showEdited: boolean = false
|
||||
@Input() original?: { [key: string]: any }
|
||||
@Output() onInputChange = new EventEmitter<void>()
|
||||
@Output() onExpand = new EventEmitter<void>()
|
||||
warningAck: { [key: string]: boolean } = {}
|
||||
unmasked: { [key: string]: boolean } = {}
|
||||
objectDisplay: { [key: string]: { expanded: boolean; height: string } } = {}
|
||||
objectDisplay: {
|
||||
[key: string]: { expanded: boolean; height: string; hasNewOptions: boolean }
|
||||
} = {}
|
||||
objectListDisplay: {
|
||||
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
|
||||
} = {}
|
||||
@@ -74,9 +76,23 @@ export class FormObjectComponent {
|
||||
}
|
||||
})
|
||||
} else if (['object', 'union'].includes(spec.type)) {
|
||||
let hasNewOptions = false
|
||||
|
||||
// We can only really show new children for objects, not unions.
|
||||
if (spec.type === 'object') {
|
||||
hasNewOptions = Object.keys(spec.spec).some(childKey => {
|
||||
const parentValue = this.original?.[key]
|
||||
return !!parentValue && parentValue[childKey] === undefined
|
||||
})
|
||||
} else if (spec.type === 'union') {
|
||||
// @TODO
|
||||
hasNewOptions = false
|
||||
}
|
||||
|
||||
this.objectDisplay[key] = {
|
||||
expanded: false,
|
||||
height: '0px',
|
||||
hasNewOptions,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -110,6 +126,7 @@ export class FormObjectComponent {
|
||||
this.objectDisplay[key] = {
|
||||
expanded: false,
|
||||
height: '0px',
|
||||
hasNewOptions: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -354,6 +371,7 @@ interface HeaderData {
|
||||
spec: ValueSpec
|
||||
edited: boolean
|
||||
new: boolean
|
||||
newOptions?: boolean
|
||||
invalid?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +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]="params.titleColor" style="font-size: xx-large; font-weight: bold;">
|
||||
{{ params.title }}
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="long-message" [innerHTML]="params.message | markdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BehaviorSubject, Subject } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'alert',
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
})
|
||||
export class AlertComponent {
|
||||
@Input() params: {
|
||||
title: string
|
||||
message: string
|
||||
titleColor: string
|
||||
}
|
||||
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
|
||||
load () { }
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<div *ngIf="loading$ | async" class="center-spinner">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<ion-label class="long-message">{{ message }}</ion-label>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BehaviorSubject, from, Subject } from 'rxjs'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { capitalizeFirstLetter } from '@start9labs/shared'
|
||||
import { markAsLoadingDuring$ } from '../loadable'
|
||||
import { WizardAction } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'complete',
|
||||
templateUrl: './complete.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
})
|
||||
export class CompleteComponent {
|
||||
@Input() params: {
|
||||
action: WizardAction
|
||||
verb: string // loader verb: '*stopping* ...'
|
||||
title: string
|
||||
executeAction: () => Promise<any>
|
||||
}
|
||||
|
||||
@Input() transitions: {
|
||||
cancel: () => any
|
||||
next: (prevResult?: any) => any
|
||||
final: () => any
|
||||
error: (e: Error) => any
|
||||
}
|
||||
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
|
||||
message: 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}`),
|
||||
),
|
||||
complete: () => this.transitions.final(),
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.message = `${capitalizeFirstLetter(this.params.verb)} ${
|
||||
this.params.title
|
||||
}...`
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<div *ngIf="loading$ | async" class="center-spinner">
|
||||
<ion-spinner color="warning" name="lines"></ion-spinner>
|
||||
<ion-label class="long-message">
|
||||
Checking for installed services which depend on
|
||||
{{ params.title }}...
|
||||
</ion-label>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!(loading$ | async) && !!dependentViolation" class="slide-content">
|
||||
<div class="wrapper">
|
||||
<div class="warning">
|
||||
<ion-label color="warning" class="label">WARNING</ion-label>
|
||||
</div>
|
||||
|
||||
<div class="long-message">
|
||||
{{ dependentViolation }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="pkgs$ | async as pkgs" class="items">
|
||||
<div class="affected">
|
||||
<ion-text color="warning">Affected Services</ion-text>
|
||||
</div>
|
||||
|
||||
<ion-item
|
||||
style="--ion-item-background: margin-top: 5px"
|
||||
*ngFor="let dep of dependentBreakages | keyvalue"
|
||||
>
|
||||
<ion-thumbnail class="thumbnail" slot="start">
|
||||
<img alt="" [src]="pkgs[dep.key]['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h5>{{ pkgs[dep.key].manifest.title }}</h5>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
.wrapper {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.items {
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.affected {
|
||||
border-width: 0 0 1px 0;
|
||||
font-size: unset;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
margin-left: 13px;
|
||||
border-style: solid;
|
||||
border-color: var(--ion-color-light-tint);
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: relative;
|
||||
height: 4vh;
|
||||
width: 4vh;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { BehaviorSubject, from, Subject } from 'rxjs'
|
||||
import { takeUntil, tap } from 'rxjs/operators'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { capitalizeFirstLetter, isEmptyObject } from '@start9labs/shared'
|
||||
import { markAsLoadingDuring$ } from '../loadable'
|
||||
import { WizardAction } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'dependents',
|
||||
templateUrl: './dependents.component.html',
|
||||
styleUrls: [
|
||||
'./dependents.component.scss',
|
||||
'../install-wizard.component.scss',
|
||||
],
|
||||
})
|
||||
export class DependentsComponent {
|
||||
@Input() params: {
|
||||
title: string
|
||||
action: WizardAction //Are you sure you want to *uninstall*...,
|
||||
verb: string // *Uninstalling* will cause problems...
|
||||
fetchBreakages: () => Promise<Breakages>
|
||||
}
|
||||
@Input() transitions: {
|
||||
cancel: () => any
|
||||
next: (prevResult?: any) => any
|
||||
final: () => any
|
||||
error: (e: Error) => any
|
||||
}
|
||||
|
||||
dependentBreakages: Breakages
|
||||
dependentViolation: string | undefined
|
||||
|
||||
loading$ = new BehaviorSubject(false)
|
||||
cancel$ = new Subject<void>()
|
||||
|
||||
readonly pkgs$ = this.patch.watch$('package-data')
|
||||
|
||||
constructor(public readonly patch: PatchDbService) {}
|
||||
|
||||
load() {
|
||||
markAsLoadingDuring$(this.loading$, from(this.params.fetchBreakages()))
|
||||
.pipe(
|
||||
takeUntil(this.cancel$),
|
||||
tap(breakages => (this.dependentBreakages = breakages)),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
console.log('DEP BREAKS, ', this.dependentBreakages)
|
||||
if (
|
||||
this.dependentBreakages &&
|
||||
!isEmptyObject(this.dependentBreakages)
|
||||
) {
|
||||
this.dependentViolation = `${capitalizeFirstLetter(
|
||||
this.params.verb,
|
||||
)} ${
|
||||
this.params.title
|
||||
} will prohibit the following services from functioning properly and may cause them to stop if they are currently running.`
|
||||
} else {
|
||||
this.transitions.next()
|
||||
}
|
||||
},
|
||||
error: (e: Error) =>
|
||||
this.transitions.error(
|
||||
new Error(
|
||||
`Fetching dependent service information failed: ${
|
||||
e.message || e
|
||||
}`,
|
||||
),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<ion-header style="height: 12vh">
|
||||
<ion-toolbar>
|
||||
<ion-label class="toolbar-label text-ellipses">
|
||||
<h1 class="toolbar-title">{{ params.toolbar.title }}</h1>
|
||||
<h3 style="font-size: large; font-style: italic">{{ params.toolbar.action }} <ion-text style="font-size: medium;">{{ params.toolbar.version | displayEmver }}</ion-text></h3>
|
||||
</ion-label>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-slides *ngIf="!error" 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) -->
|
||||
<alert #components *ngIf="def.slide.selector === 'alert'" [params]="def.slide.params" style="width: 100%;"></alert>
|
||||
<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" class="slide-content">
|
||||
<div style="margin-top: 25px;">
|
||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
|
||||
Error
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="long-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar style="padding: 8px;">
|
||||
<ng-container *ngIf="!initializing && !error">
|
||||
|
||||
<!-- cancel button if loading/not loading -->
|
||||
<ion-button slot="start" *ngIf="(currentSlide.loading$ | async) && currentBottomBar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline">
|
||||
<ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text.length > 16">{{ cancel.text }}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="start" *ngIf="!(currentSlide.loading$ | async) && currentBottomBar.cancel.afterLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline">
|
||||
<ion-text *ngIf="cancel.text" [class.smaller-text]="cancel.text.length > 16">{{ cancel.text }}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- next/finish buttons -->
|
||||
<ng-container *ngIf="!(currentSlide.loading$ | async)">
|
||||
<!-- next -->
|
||||
<ion-button slot="end" *ngIf="currentBottomBar.next as next" (click)="callTransition(transitions.next)" fill="outline" class="toolbar-button enter-click" [class.no-click]="transitioning">
|
||||
<ion-text [class.smaller-text]="next.length > 16">{{ next }}</ion-text>
|
||||
</ion-button>
|
||||
|
||||
<!-- finish -->
|
||||
<ion-button slot="end" *ngIf="currentBottomBar.finish as finish" (click)="callTransition(transitions.final)" fill="outline" class="toolbar-button enter-click" [class.no-click]="transitioning">
|
||||
<ion-text [class.smaller-text]="finish.length > 16">{{ finish }}</ion-text>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
<ng-container *ngIf="error">
|
||||
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
||||
</ng-container>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
@@ -1,86 +0,0 @@
|
||||
.toolbar-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: white;
|
||||
padding: 8px 0px 8px 15px;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-size: x-large;
|
||||
text-transform: capitalize;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #404040;
|
||||
font-family: 'Montserrat';
|
||||
}
|
||||
|
||||
.center-spinner {
|
||||
min-height: 40vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
color:white;
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
color:white;
|
||||
min-height: 40vh
|
||||
}
|
||||
|
||||
.notes-content {
|
||||
text-align: left;
|
||||
margin: 32px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.long-message {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding: 10px;
|
||||
font-size: small;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (min-width:500px) {
|
||||
.long-message {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding: 10px;
|
||||
font-size: medium;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
text-transform: capitalize;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.smaller-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
width: 2vh;
|
||||
height: 2vh;
|
||||
border-radius: 50px;
|
||||
left: -0.75vh;
|
||||
top: -0.75vh;
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
NgZone,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
||||
import { capitalizeFirstLetter, pauseFor } from '@start9labs/shared'
|
||||
import { CompleteComponent } from './complete/complete.component'
|
||||
import { DependentsComponent } from './dependents/dependents.component'
|
||||
import { AlertComponent } from './alert/alert.component'
|
||||
import { NotesComponent } from './notes/notes.component'
|
||||
import { Loadable } from './loadable'
|
||||
import { WizardAction } from './wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'install-wizard',
|
||||
templateUrl: './install-wizard.component.html',
|
||||
styleUrls: ['./install-wizard.component.scss'],
|
||||
})
|
||||
export class InstallWizardComponent {
|
||||
transitioning = false
|
||||
|
||||
@Input() params: {
|
||||
// defines each slide along with bottom bar
|
||||
slideDefinitions: SlideDefinition[]
|
||||
toolbar: TopbarParams
|
||||
}
|
||||
|
||||
// content container so we can scroll to top between slide transitions
|
||||
@ViewChild(IonContent) contentContainer: IonContent
|
||||
// slide container gives us hook into ion-slide, allowing for slide transitions
|
||||
@ViewChild(IonSlides) slideContainer: IonSlides
|
||||
|
||||
//a slide component gives us hook into a slide. Allows us to call load when slide comes into view
|
||||
@ViewChildren('components')
|
||||
slideComponentsQL: QueryList<Loadable>
|
||||
get slideComponents(): Loadable[] {
|
||||
return this.slideComponentsQL.toArray()
|
||||
}
|
||||
|
||||
private slideIndex = 0
|
||||
get currentSlide(): Loadable {
|
||||
return this.slideComponents[this.slideIndex]
|
||||
}
|
||||
get currentBottomBar(): SlideDefinition['bottomBar'] {
|
||||
return this.params.slideDefinitions[this.slideIndex].bottomBar
|
||||
}
|
||||
|
||||
initializing = true
|
||||
error = ''
|
||||
|
||||
constructor(
|
||||
private readonly modalController: ModalController,
|
||||
private readonly zone: NgZone,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.currentSlide.load()
|
||||
this.slideContainer.update()
|
||||
this.slideContainer.lockSwipes(true)
|
||||
}
|
||||
|
||||
ionViewDidEnter() {
|
||||
this.initializing = 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.final || i.cancelled) return this.modalController.dismiss(i)
|
||||
if (i.error) return (this.error = capitalizeFirstLetter(i.error.message))
|
||||
|
||||
this.moveToNextSlide(i.next)
|
||||
}
|
||||
|
||||
// bottom bar button callbacks. Pass this into components if they need to trigger slide transitions independent of the bottom bar clicks
|
||||
transitions = {
|
||||
next: (prevResult: any) =>
|
||||
this.transition({ next: prevResult || this.currentSlide.result }),
|
||||
cancel: () => this.transition({ cancelled: true }),
|
||||
final: () => this.transition({ final: true }),
|
||||
error: (e: Error) => this.transition({ error: e }),
|
||||
}
|
||||
|
||||
private async moveToNextSlide(prevResult?: any) {
|
||||
if (this.slideComponents[this.slideIndex + 1] === undefined) {
|
||||
return this.transition({ final: true })
|
||||
}
|
||||
this.zone.run(async () => {
|
||||
this.slideComponents[this.slideIndex + 1].load(prevResult)
|
||||
await pauseFor(50) // give the load ^ opportunity to propogate into slide before sliding it into view
|
||||
this.slideIndex += 1
|
||||
await this.slideContainer.lockSwipes(false)
|
||||
await this.contentContainer.scrollToTop()
|
||||
await this.slideContainer.slideNext(500)
|
||||
await this.slideContainer.lockSwipes(true)
|
||||
})
|
||||
}
|
||||
|
||||
async callTransition(transition: Function) {
|
||||
this.transitioning = true
|
||||
await transition()
|
||||
this.transitioning = false
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlideDefinition {
|
||||
slide:
|
||||
| { selector: 'dependents'; params: DependentsComponent['params'] }
|
||||
| { selector: 'complete'; params: CompleteComponent['params'] }
|
||||
| { selector: 'alert'; params: AlertComponent['params'] }
|
||||
| { 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 }
|
||||
}
|
||||
// indicates the existence of next or finish buttons (should only have one)
|
||||
next?: string
|
||||
finish?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type TopbarParams = {
|
||||
action: WizardAction
|
||||
title: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export async function wizardModal(
|
||||
modalController: ModalController,
|
||||
params: InstallWizardComponent['params'],
|
||||
): Promise<{ cancelled?: true; final?: true; modal: HTMLIonModalElement }> {
|
||||
const modal = await modalController.create({
|
||||
backdropDismiss: false,
|
||||
cssClass: 'wizard-modal',
|
||||
component: InstallWizardComponent,
|
||||
componentProps: { params },
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
return modal.onWillDismiss().then(({ data }) => ({ ...data, modal }))
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs'
|
||||
import { concatMap, finalize } from 'rxjs/operators'
|
||||
import { fromSync$, emitAfter$ } from 'src/app/util/rxjs.util'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function markAsLoadingDuring$<T> (trigger$: Subject<boolean>, o: Observable<T>): Observable<T> {
|
||||
let shouldBeOn = true
|
||||
const displayIfItsBeenAtLeast = 5 // ms
|
||||
return fromSync$(() => {
|
||||
emitAfter$(displayIfItsBeenAtLeast).subscribe(() => { if (shouldBeOn) trigger$.next(true) })
|
||||
}).pipe(
|
||||
concatMap(() => o),
|
||||
finalize(() => {
|
||||
trigger$.next(false)
|
||||
shouldBeOn = false
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<div class="slide-content">
|
||||
<div class="notes-content">
|
||||
<h1>{{ params.title }}</h1>
|
||||
<br />
|
||||
<div *ngFor="let v of params.versions">
|
||||
<h4>
|
||||
<b>
|
||||
{{ v.version }}
|
||||
</b>
|
||||
</h4>
|
||||
<hr style="height: 0; border-width: 1px" />
|
||||
<div [innerHTML]="v.notes | markdown"></div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,395 +0,0 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { exists } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from '../../services/api/embassy-api.service'
|
||||
import {
|
||||
InstallWizardComponent,
|
||||
SlideDefinition,
|
||||
TopbarParams,
|
||||
} from './install-wizard.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { first } from 'rxjs/operators'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WizardBaker {
|
||||
constructor(
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly config: ConfigService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
update(values: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, installAlert } = values
|
||||
|
||||
const action = 'update'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: Array<SlideDefinition | undefined> = [
|
||||
installAlert
|
||||
? {
|
||||
slide: {
|
||||
selector: 'alert',
|
||||
params: {
|
||||
title: 'Warning',
|
||||
message: installAlert,
|
||||
titleColor: 'warning',
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Next',
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
action,
|
||||
verb: 'updating',
|
||||
title,
|
||||
fetchBreakages: () =>
|
||||
this.embassyApi
|
||||
.dryUpdatePackage({ id, version })
|
||||
.then(breakages => breakages),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Update Anyway',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action,
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
executeAction: () =>
|
||||
this.marketplaceService
|
||||
.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
.pipe(first())
|
||||
.toPromise(),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { whileLoading: {} },
|
||||
finish: 'Dismiss',
|
||||
},
|
||||
},
|
||||
]
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
updateOS(values: {
|
||||
version: string
|
||||
releaseNotes: { [version: string]: string }
|
||||
headline: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { version, releaseNotes, headline } = values
|
||||
|
||||
const versions = Object.keys(releaseNotes)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(version => {
|
||||
return {
|
||||
version,
|
||||
notes: releaseNotes[version],
|
||||
}
|
||||
})
|
||||
|
||||
const action = 'update'
|
||||
const title = 'EmbassyOS'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{
|
||||
slide: {
|
||||
selector: 'notes',
|
||||
params: {
|
||||
versions,
|
||||
title: 'Release Notes',
|
||||
titleColor: 'dark',
|
||||
headline,
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Begin Update',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action,
|
||||
verb: 'beginning update for',
|
||||
title,
|
||||
executeAction: () =>
|
||||
this.embassyApi.updateServer({
|
||||
'marketplace-url': this.config.marketplace.url,
|
||||
}),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { whileLoading: {} },
|
||||
finish: 'Dismiss',
|
||||
},
|
||||
},
|
||||
]
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
downgrade(values: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, installAlert } = values
|
||||
|
||||
const action = 'downgrade'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: Array<SlideDefinition | undefined> = [
|
||||
installAlert
|
||||
? {
|
||||
slide: {
|
||||
selector: 'alert',
|
||||
params: {
|
||||
title: 'Warning',
|
||||
message: installAlert,
|
||||
titleColor: 'warning',
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Next',
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
action,
|
||||
verb: 'downgrading',
|
||||
title,
|
||||
fetchBreakages: () =>
|
||||
this.embassyApi
|
||||
.dryUpdatePackage({ id, version })
|
||||
.then(breakages => breakages),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
whileLoading: {},
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Downgrade Anyway',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action,
|
||||
verb: 'beginning downgrade for',
|
||||
title,
|
||||
executeAction: () =>
|
||||
this.marketplaceService
|
||||
.installPackage({
|
||||
id,
|
||||
'version-spec': version ? `=${version}` : undefined,
|
||||
})
|
||||
.pipe(first())
|
||||
.toPromise(),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: { whileLoading: {} },
|
||||
finish: 'Dismiss',
|
||||
},
|
||||
},
|
||||
]
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
uninstall(values: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
uninstallAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { id, title, version, uninstallAlert } = values
|
||||
|
||||
const action = 'uninstall'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{
|
||||
slide: {
|
||||
selector: 'alert',
|
||||
params: {
|
||||
title: 'Warning',
|
||||
message: uninstallAlert || defaultUninstallWarning(title),
|
||||
titleColor: 'warning',
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Continue',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
action,
|
||||
verb: 'uninstalling',
|
||||
title,
|
||||
fetchBreakages: () =>
|
||||
this.embassyApi
|
||||
.dryUninstallPackage({ id })
|
||||
.then(breakages => breakages),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
whileLoading: {},
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Uninstall',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action,
|
||||
verb: 'uninstalling',
|
||||
title,
|
||||
executeAction: () => this.embassyApi.uninstallPackage({ id }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
finish: 'Dismiss',
|
||||
cancel: {
|
||||
whileLoading: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
stop(values: {
|
||||
id: string
|
||||
title: string
|
||||
version: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { title, version, id } = values
|
||||
|
||||
const action = 'stop'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
action,
|
||||
verb: 'stopping',
|
||||
title,
|
||||
fetchBreakages: () =>
|
||||
this.embassyApi
|
||||
.dryStopPackage({ id })
|
||||
.then(breakages => breakages),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
whileLoading: {},
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Stop Anyway',
|
||||
},
|
||||
},
|
||||
{
|
||||
slide: {
|
||||
selector: 'complete',
|
||||
params: {
|
||||
action,
|
||||
verb: 'stopping',
|
||||
title,
|
||||
executeAction: () => this.embassyApi.stopPackage({ id }),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
finish: 'Dismiss',
|
||||
cancel: {
|
||||
whileLoading: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
|
||||
configure(values: {
|
||||
pkg: PackageDataEntry
|
||||
breakages: Breakages
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { breakages, pkg } = values
|
||||
const { title, version } = pkg.manifest
|
||||
const action = 'configure'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{
|
||||
slide: {
|
||||
selector: 'dependents',
|
||||
params: {
|
||||
action,
|
||||
verb: 'saving config for',
|
||||
title,
|
||||
fetchBreakages: () => Promise.resolve(breakages),
|
||||
},
|
||||
},
|
||||
bottomBar: {
|
||||
cancel: {
|
||||
afterLoading: { text: 'Cancel' },
|
||||
},
|
||||
next: 'Save Config Anyway',
|
||||
},
|
||||
},
|
||||
]
|
||||
return { toolbar, slideDefinitions }
|
||||
}
|
||||
}
|
||||
|
||||
const defaultUninstallWarning = (serviceName: string) =>
|
||||
`Uninstalling ${serviceName} will result in the deletion of its data.`
|
||||
@@ -1,7 +0,0 @@
|
||||
export type WizardAction =
|
||||
'install'
|
||||
| 'update'
|
||||
| 'downgrade'
|
||||
| 'uninstall'
|
||||
| 'stop'
|
||||
| 'configure'
|
||||
@@ -1,21 +1,11 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Config</ion-title>
|
||||
<ion-buttons
|
||||
*ngIf="!loadingText && !loadingError && hasConfig"
|
||||
slot="end"
|
||||
class="ion-padding-end"
|
||||
>
|
||||
<ion-button fill="clear" (click)="resetDefaults()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Reset Defaults
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -91,7 +81,7 @@
|
||||
[objectSpec]="configSpec"
|
||||
[formGroup]="configForm"
|
||||
[current]="configForm.value"
|
||||
[showEdited]="true"
|
||||
[original]="original"
|
||||
></form-object>
|
||||
</form>
|
||||
</ng-template>
|
||||
@@ -100,6 +90,16 @@
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-buttons
|
||||
*ngIf="!loadingText && !loadingError && hasConfig"
|
||||
slot="start"
|
||||
class="ion-padding-start"
|
||||
>
|
||||
<ion-button fill="clear" (click)="resetDefaults()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Reset Defaults
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-buttons
|
||||
*ngIf="!loadingText && !loadingError"
|
||||
slot="end"
|
||||
|
||||
@@ -10,12 +10,11 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
ErrorToastService,
|
||||
getErrorMessage,
|
||||
isEmptyObject,
|
||||
isObject,
|
||||
} from '@start9labs/shared'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from 'src/app/components/app-wizard/app-wizard.component'
|
||||
import { WizardDefs } from 'src/app/components/app-wizard/wizard-defs'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
@@ -46,7 +45,7 @@ export class AppConfigPage {
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
constructor(
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly wizards: WizardDefs,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
@@ -138,6 +137,13 @@ export class AppConfigPage {
|
||||
return
|
||||
}
|
||||
|
||||
const hasDependents = !!Object.keys(
|
||||
this.pkg?.installed?.['current-dependents'] || {},
|
||||
).filter(depId => depId !== this.pkgId).length
|
||||
|
||||
const config = this.configForm.value
|
||||
|
||||
if (!hasDependents) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: `Saving config. This could take a while...`,
|
||||
@@ -147,24 +153,6 @@ export class AppConfigPage {
|
||||
this.saving = true
|
||||
|
||||
try {
|
||||
const config = this.configForm.value
|
||||
|
||||
const breakages = await this.embassyApi.drySetPackageConfig({
|
||||
id: this.pkgId,
|
||||
config,
|
||||
})
|
||||
|
||||
if (!isEmptyObject(breakages['length']) && this.pkg) {
|
||||
const { cancelled } = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.configure({
|
||||
pkg: this.pkg,
|
||||
breakages,
|
||||
}),
|
||||
)
|
||||
if (cancelled) return
|
||||
}
|
||||
|
||||
await this.embassyApi.setPackageConfig({
|
||||
id: this.pkgId,
|
||||
config,
|
||||
@@ -176,6 +164,19 @@ export class AppConfigPage {
|
||||
this.saving = false
|
||||
loader.dismiss()
|
||||
}
|
||||
} else {
|
||||
const success = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizards.configure({
|
||||
manifest: this.pkg!.manifest,
|
||||
config,
|
||||
}),
|
||||
)
|
||||
|
||||
if (success) {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getDiff(patch: Operation[]): string[] {
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from 'src/app/components/app-wizard/app-wizard.component'
|
||||
import { WizardDefs } from 'src/app/components/app-wizard/wizard-defs'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||
@@ -39,7 +39,7 @@ export class AppActionsPage {
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly wizards: WizardDefs,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDbService,
|
||||
) {}
|
||||
@@ -137,20 +137,20 @@ export class AppActionsPage {
|
||||
}
|
||||
|
||||
async uninstall() {
|
||||
const { id, title, version, alerts } = this.pkg.manifest
|
||||
const data = await wizardModal(
|
||||
const { id, title, alerts } = this.pkg.manifest
|
||||
const success = await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.uninstall({
|
||||
this.wizards.uninstall({
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
uninstallAlert: alerts.uninstall || undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
if (data.cancelled) return
|
||||
if (success) {
|
||||
return this.navCtrl.navigateRoot('/services')
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(
|
||||
actionId: string,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppShowPage } from './app-show.page'
|
||||
import { EmverPipesModule } from '@start9labs/shared'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { AppWizardComponentModule } from 'src/app/components/app-wizard/app-wizard.component.module'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
|
||||
@@ -50,7 +50,7 @@ const routes: Routes = [
|
||||
StatusComponentModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
InstallWizardComponentModule,
|
||||
AppWizardComponentModule,
|
||||
AppConfigPageModule,
|
||||
EmverPipesModule,
|
||||
LaunchablePipeModule,
|
||||
|
||||
@@ -12,15 +12,14 @@ import {
|
||||
Status,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { wizardModal } from 'src/app/components/app-wizard/app-wizard.component'
|
||||
import { WizardDefs } from 'src/app/components/app-wizard/wizard-defs'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
|
||||
@@ -51,8 +50,7 @@ export class AppShowStatusComponent {
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly wizards: WizardDefs,
|
||||
private readonly launcherService: UiLauncherService,
|
||||
private readonly modalService: ModalService,
|
||||
) {}
|
||||
@@ -108,7 +106,7 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
const { id, title, version } = this.pkg.manifest
|
||||
const { id, title } = this.pkg.manifest
|
||||
const hasDependents = !!Object.keys(
|
||||
this.pkg.installed?.['current-dependents'] || {},
|
||||
).filter(depId => depId !== id).length
|
||||
@@ -130,10 +128,9 @@ export class AppShowStatusComponent {
|
||||
} else {
|
||||
wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.stop({
|
||||
this.wizards.stop({
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from 'src/app/components/app-wizard/app-wizard.component'
|
||||
import { WizardDefs } from 'src/app/components/app-wizard/wizard-defs'
|
||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
|
||||
@Component({
|
||||
@@ -33,7 +33,7 @@ export class MarketplaceShowControlsComponent {
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly wizards: WizardDefs,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
public readonly localStorageService: LocalStorageService,
|
||||
@@ -81,16 +81,11 @@ export class MarketplaceShowControlsComponent {
|
||||
installAlert: alerts.install || undefined,
|
||||
}
|
||||
|
||||
const { cancelled } = await wizardModal(
|
||||
wizardModal(
|
||||
this.modalCtrl,
|
||||
action === 'update'
|
||||
? this.wizardBaker.update(value)
|
||||
: this.wizardBaker.downgrade(value),
|
||||
? this.wizards.update(value)
|
||||
: this.wizards.downgrade(value),
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
await pauseFor(250)
|
||||
this.navCtrl.back()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
AdditionalModule,
|
||||
DependenciesModule,
|
||||
} from '@start9labs/marketplace'
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { AppWizardComponentModule } from 'src/app/components/app-wizard/app-wizard.component.module'
|
||||
|
||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
||||
@@ -39,7 +39,7 @@ const routes: Routes = [
|
||||
EmverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
MarketplaceStatusModule,
|
||||
InstallWizardComponentModule,
|
||||
AppWizardComponentModule,
|
||||
PackageModule,
|
||||
AboutModule,
|
||||
DependenciesModule,
|
||||
|
||||
@@ -11,8 +11,8 @@ import { ActivatedRoute } from '@angular/router'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { filter, map, take } from 'rxjs/operators'
|
||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
import { wizardModal } from 'src/app/components/app-wizard/app-wizard.component'
|
||||
import { WizardDefs } from 'src/app/components/app-wizard/wizard-defs'
|
||||
import { exists, isEmptyObject, ErrorToastService } from '@start9labs/shared'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { LocalStorageService } from 'src/app/services/local-storage.service'
|
||||
@@ -32,7 +32,7 @@ export class ServerShowPage {
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly wizards: WizardDefs,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly embassyApi: ApiService,
|
||||
@@ -70,7 +70,7 @@ export class ServerShowPage {
|
||||
|
||||
await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.updateOS({
|
||||
this.wizards.updateOS({
|
||||
version,
|
||||
headline,
|
||||
releaseNotes,
|
||||
|
||||
@@ -55,8 +55,7 @@ export module Mock {
|
||||
uninstall:
|
||||
'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.',
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: 'Stopping Bitcoin is bad for your health.',
|
||||
start: 'Starting Bitcoin is good for your health.',
|
||||
},
|
||||
main: {
|
||||
type: 'docker',
|
||||
@@ -354,7 +353,6 @@ export module Mock {
|
||||
restore:
|
||||
'If this is a duplicate instance of the same LND node, you may loose your funds.',
|
||||
start: 'Starting LND is good for your health.',
|
||||
stop: null,
|
||||
},
|
||||
main: {
|
||||
type: 'docker',
|
||||
@@ -499,7 +497,6 @@ export module Mock {
|
||||
uninstall: null,
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: null,
|
||||
},
|
||||
main: {
|
||||
type: 'docker',
|
||||
@@ -1662,10 +1659,8 @@ export module Mock {
|
||||
},
|
||||
}
|
||||
|
||||
export const MockConfig = {}
|
||||
|
||||
export const MockDependencyConfig = {
|
||||
testnet: true,
|
||||
export const MockConfig = {
|
||||
testnet: undefined,
|
||||
'object-list': [
|
||||
{
|
||||
'first-name': 'First',
|
||||
@@ -1691,19 +1686,19 @@ export module Mock {
|
||||
law1: 'The first law Amended',
|
||||
law2: 'The second law',
|
||||
},
|
||||
rpcpass: null,
|
||||
rpcpass: undefined,
|
||||
rpcuser: '123',
|
||||
rulemakers: [],
|
||||
},
|
||||
advanced: {
|
||||
notifications: ['email', 'text', 'push'],
|
||||
},
|
||||
'bitcoin-node': undefined,
|
||||
port: 20,
|
||||
rpcallowip: undefined,
|
||||
rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'],
|
||||
advanced: undefined,
|
||||
}
|
||||
|
||||
export const MockDependencyConfig = MockConfig
|
||||
|
||||
export const bitcoind: PackageDataEntry = {
|
||||
state: PackageState.Installed,
|
||||
'static-files': {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
name: `Matt's Embassy`,
|
||||
'auto-check-updates': false,
|
||||
'auto-check-updates': true,
|
||||
'pkg-order': [],
|
||||
'ack-welcome': '1.0.0',
|
||||
},
|
||||
@@ -67,8 +67,7 @@ export const mockPatchData: DataModel = {
|
||||
uninstall:
|
||||
'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.',
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: 'Stopping Bitcoin is bad for your health.',
|
||||
start: 'Starting Bitcoin is good for your health.',
|
||||
},
|
||||
main: {
|
||||
type: 'docker',
|
||||
@@ -449,7 +448,6 @@ export const mockPatchData: DataModel = {
|
||||
restore:
|
||||
'If this is a duplicate instance of the same LND node, you may loose your funds.',
|
||||
start: 'Starting LND is good for your health.',
|
||||
stop: null,
|
||||
},
|
||||
main: {
|
||||
type: 'docker',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@import '~swiper/scss';
|
||||
@import '~@ionic/angular/css/ionic-swiper';
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
@@ -51,6 +54,12 @@
|
||||
|
||||
$subheader-height: 48px;
|
||||
|
||||
.swiper {
|
||||
.swiper-slide {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.subheader-padding {
|
||||
--padding-top: #{$subheader-height} + 10px;
|
||||
}
|
||||
@@ -209,13 +218,6 @@ ion-button {
|
||||
}
|
||||
}
|
||||
|
||||
ion-slides {
|
||||
.slider-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
ion-back-button {
|
||||
--background: var(--ion-color-light);
|
||||
--color: var(--ion-color-dark);
|
||||
|
||||
Reference in New Issue
Block a user