diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a242987b0..353d5496b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index a5275347c..0efd74c5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/projects/marketplace/src/types/marketplace-manifest.ts b/frontend/projects/marketplace/src/types/marketplace-manifest.ts index 1d0342119..e180ae3ea 100644 --- a/frontend/projects/marketplace/src/types/marketplace-manifest.ts +++ b/frontend/projects/marketplace/src/types/marketplace-manifest.ts @@ -22,7 +22,6 @@ export interface MarketplaceManifest { uninstall: string | null restore: string | null start: string | null - stop: string | null } dependencies: Record> } diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.html b/frontend/projects/ui/src/app/app/preloader/preloader.component.html index 7d486676b..24e0aca96 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.html +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.html @@ -3,6 +3,9 @@ + + Slide 1 + @@ -46,7 +49,6 @@ - load bold font diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.module.ts b/frontend/projects/ui/src/app/app/preloader/preloader.module.ts index c5a7bcf47..2ba1eff56 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.module.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.module.ts @@ -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], diff --git a/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.html b/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.html new file mode 100644 index 000000000..2fc8bae95 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.html @@ -0,0 +1,4 @@ +

+ Warning +

+
diff --git a/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.module.ts b/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.module.ts similarity index 100% rename from frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.module.ts rename to frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.module.ts diff --git a/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.ts b/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.ts new file mode 100644 index 000000000..ed1e1e788 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/alert/alert.component.ts @@ -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 +} diff --git a/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.html b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.html new file mode 100644 index 000000000..b9a9d450b --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.html @@ -0,0 +1,90 @@ + + +
+ {{ params.title }} +
+ + {{ params.action | titlecase + }}: {{ params.version | displayEmver }} + +
+ + + + + +
+
+ + +
+ + + + + + + + + + +

+ {{ error }} +

+
+
+
+ + + + + + + Dismiss + + + + Continue + + + + + + diff --git a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.module.ts b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.module.ts similarity index 76% rename from frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.module.ts rename to frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.module.ts index a40e85c66..7de617cc2 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.module.ts +++ b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.scss b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.scss new file mode 100644 index 000000000..f88ae0ac1 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.scss @@ -0,0 +1,6 @@ +.underline { + margin: 6px 0 8px 16px; + border-style: solid; + border-width: 0px 0px 1px 0px; + border-color: #404040; +} diff --git a/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.ts b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.ts new file mode 100644 index 000000000..22245cedf --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/app-wizard.component.ts @@ -0,0 +1,106 @@ +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 + + 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.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 { + const modal = await modalController.create({ + backdropDismiss: false, + cssClass: 'wizard-modal', + component: AppWizardComponent, + componentProps: { params }, + }) + + await modal.present() + return modal.onDidDismiss().then(({ role }) => role === 'success') +} diff --git a/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.html b/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.html new file mode 100644 index 000000000..88bda51df --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.html @@ -0,0 +1,4 @@ +
+ +

{{ message }}

+
diff --git a/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.module.ts b/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.module.ts similarity index 62% rename from frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.module.ts rename to frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.module.ts index fd0ac3ddd..a8cb7ba39 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.module.ts +++ b/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.module.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.ts b/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.ts new file mode 100644 index 000000000..6d0dfdd1d --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/complete/complete.component.ts @@ -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 + } + + @Output() onSuccess: EventEmitter = new EventEmitter() + @Output() onError: EventEmitter = 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}`) + } + } +} diff --git a/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.html b/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.html new file mode 100644 index 000000000..997a80595 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.html @@ -0,0 +1,25 @@ +
+ +

Checking for installed services which depend on {{ params.title }}...

+
+ + +

Warning

+

{{ warningMessage }}

+ + + + + Affected Services + + + + + + + {{ pkgs[dep.key].manifest.title }} + + + + +
diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.module.ts b/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.module.ts similarity index 100% rename from frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.module.ts rename to frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.module.ts diff --git a/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.scss b/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.ts b/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.ts new file mode 100644 index 000000000..2edc3ede9 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/dependents/dependents.component.ts @@ -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 + } + + @Output() onSuccess: EventEmitter = new EventEmitter() + @Output() onError: EventEmitter = 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 + } + } +} diff --git a/frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.html b/frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.html new file mode 100644 index 000000000..8d521d0c8 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.html @@ -0,0 +1,14 @@ +
+

Release Notes

+
+
+

+ + {{ v.version }} + +

+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.module.ts b/frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.module.ts similarity index 100% rename from frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.module.ts rename to frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.module.ts diff --git a/frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.ts b/frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.ts similarity index 50% rename from frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.ts rename to frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.ts index c2ab3fcea..e53049e8e 100644 --- a/frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.ts +++ b/frontend/projects/ui/src/app/components/app-wizard/notes/notes.component.ts @@ -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() + loading = false + + async load() {} asIsOrder() { return 0 diff --git a/frontend/projects/ui/src/app/components/app-wizard/wizard-defs.ts b/frontend/projects/ui/src/app/components/app-wizard/wizard-defs.ts new file mode 100644 index 000000000..e4fb16894 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/wizard-defs.ts @@ -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 = [ + 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 = [ + 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.` diff --git a/frontend/projects/ui/src/app/components/app-wizard/wizard-types.ts b/frontend/projects/ui/src/app/components/app-wizard/wizard-types.ts new file mode 100644 index 000000000..9f6cca859 --- /dev/null +++ b/frontend/projects/ui/src/app/components/app-wizard/wizard-types.ts @@ -0,0 +1,11 @@ +export type WizardAction = + | 'update' + | 'downgrade' + | 'uninstall' + | 'stop' + | 'configure' + +export interface BaseSlide { + load: () => Promise + loading: boolean +} diff --git a/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.html b/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.html deleted file mode 100644 index ab1f00ec8..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
-
-
- - {{ params.title }} - -
-
-
-
diff --git a/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.ts b/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.ts deleted file mode 100644 index 9e7b0f1c6..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/alert/alert.component.ts +++ /dev/null @@ -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() - - load () { } -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.html b/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.html deleted file mode 100644 index d918c3833..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - {{ message }} -
\ No newline at end of file diff --git a/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.ts b/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.ts deleted file mode 100644 index d88b53465..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/complete/complete.component.ts +++ /dev/null @@ -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 - } - - @Input() transitions: { - cancel: () => any - next: (prevResult?: any) => any - final: () => any - error: (e: Error) => any - } - - loading$ = new BehaviorSubject(false) - cancel$ = new Subject() - - 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 - }...` - } -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html deleted file mode 100644 index afeca4d7c..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.html +++ /dev/null @@ -1,37 +0,0 @@ -
- - - Checking for installed services which depend on - {{ params.title }}... - -
- -
-
-
- WARNING -
- -
- {{ dependentViolation }} -
- -
-
- Affected Services -
- - - - - - -
{{ pkgs[dep.key].manifest.title }}
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.scss b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.scss deleted file mode 100644 index 051ba8121..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.scss +++ /dev/null @@ -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; -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts b/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts deleted file mode 100644 index af31bc3a3..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/dependents/dependents.component.ts +++ /dev/null @@ -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 - } - @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() - - 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 - }`, - ), - ), - }) - } -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.html b/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.html deleted file mode 100644 index d7c27e34c..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.html +++ /dev/null @@ -1,67 +0,0 @@ - - - -

{{ params.toolbar.title }}

-

{{ params.toolbar.action }} {{ params.toolbar.version | displayEmver }}

-
-
-
- - - - - - - - - - - - -
-
-
- - Error - -
-
- {{ error }} -
-
-
-
- - - - - - - - {{ cancel.text }} - - - - {{ cancel.text }} - - - - - - - - {{ next }} - - - - - {{ finish }} - - - - - - Dismiss - - - diff --git a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.scss b/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.scss deleted file mode 100644 index bde9fe986..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.scss +++ /dev/null @@ -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; -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.ts b/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.ts deleted file mode 100644 index 94e74c8c0..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/install-wizard.component.ts +++ /dev/null @@ -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 - 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 })) -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/loadable.ts b/frontend/projects/ui/src/app/components/install-wizard/loadable.ts deleted file mode 100644 index fd8db92d1..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/loadable.ts +++ /dev/null @@ -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 // will be true during load function - cancel$: Subject // will cancel load function -} - -export function markAsLoadingDuring$ (trigger$: Subject, o: Observable): Observable { - 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 - }), - ) -} diff --git a/frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.html b/frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.html deleted file mode 100644 index b0245a7a3..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/notes/notes.component.html +++ /dev/null @@ -1,16 +0,0 @@ -
-
-

{{ params.title }}

-
-
-

- - {{ v.version }} - -

-
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts b/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts deleted file mode 100644 index 332b2ec21..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/prebaked-wizards.ts +++ /dev/null @@ -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 = [ - 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 = [ - 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.` diff --git a/frontend/projects/ui/src/app/components/install-wizard/wizard-types.ts b/frontend/projects/ui/src/app/components/install-wizard/wizard-types.ts deleted file mode 100644 index f7e31c953..000000000 --- a/frontend/projects/ui/src/app/components/install-wizard/wizard-types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type WizardAction = - 'install' - | 'update' - | 'downgrade' - | 'uninstall' - | 'stop' - | 'configure' \ No newline at end of file diff --git a/frontend/projects/ui/src/app/modals/app-config/app-config.page.html b/frontend/projects/ui/src/app/modals/app-config/app-config.page.html index 35dbd29a2..c8d5d2fef 100644 --- a/frontend/projects/ui/src/app/modals/app-config/app-config.page.html +++ b/frontend/projects/ui/src/app/modals/app-config/app-config.page.html @@ -1,21 +1,11 @@ - + Config + - Config - - - - Reset Defaults - - @@ -100,6 +90,16 @@

+ + + + Reset Defaults + + depId !== this.pkgId).length - try { - const config = this.configForm.value + const config = this.configForm.value - const breakages = await this.embassyApi.drySetPackageConfig({ - id: this.pkgId, - config, + if (!hasDependents) { + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + message: `Saving config. This could take a while...`, }) + await loader.present() + + this.saving = true - if (!isEmptyObject(breakages['length']) && this.pkg) { - const { cancelled } = await wizardModal( - this.modalCtrl, - this.wizardBaker.configure({ - pkg: this.pkg, - breakages, - }), - ) - if (cancelled) return + try { + await this.embassyApi.setPackageConfig({ + id: this.pkgId, + config, + }) + this.modalCtrl.dismiss() + } catch (e: any) { + this.errToast.present(e) + } finally { + this.saving = false + loader.dismiss() } + } else { + const success = await wizardModal( + this.modalCtrl, + this.wizards.configure({ + manifest: this.pkg!.manifest, + config, + }), + ) - await this.embassyApi.setPackageConfig({ - id: this.pkgId, - config, - }) - this.modalCtrl.dismiss() - } catch (e: any) { - this.errToast.present(e) - } finally { - this.saving = false - loader.dismiss() + if (success) { + this.modalCtrl.dismiss() + } } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index b7cb62f9e..33332fcfb 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -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,19 +137,19 @@ 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 - return this.navCtrl.navigateRoot('/services') + if (success) { + return this.navCtrl.navigateRoot('/services') + } } private async executeAction( diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 0aa30ef9a..08794ca7f 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -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, diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index 6ce783dba..086413a8c 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -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 { - 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, }), ) } diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts index 7f111a6a7..edad27bf5 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -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,14 +81,14 @@ export class MarketplaceShowControlsComponent { installAlert: alerts.install || undefined, } - const { cancelled } = await wizardModal( + const success = await wizardModal( this.modalCtrl, action === 'update' - ? this.wizardBaker.update(value) - : this.wizardBaker.downgrade(value), + ? this.wizards.update(value) + : this.wizards.downgrade(value), ) - if (cancelled) return + if (!success) return await pauseFor(250) this.navCtrl.back() diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index 985d8b1fa..685f0d77f 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -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, diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index d615996ca..8043b8c65 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -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, diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index c45e6b9fb..aee684fc9 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -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', diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 5ae9714e6..bc700f6df 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -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', diff --git a/frontend/projects/ui/src/styles.scss b/frontend/projects/ui/src/styles.scss index 271430480..5573723f8 100644 --- a/frontend/projects/ui/src/styles.scss +++ b/frontend/projects/ui/src/styles.scss @@ -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);