0.3.0 refactor

ui: adds overlay layer to patch-db-client

ui: getting towards mocks

ui: cleans up factory init

ui: nice type hack

ui: live api for patch

ui: api service source + http

starts up

ui: api source + http

ui: rework patchdb config, pass stashTimeout into patchDbModel

wires in temp patching into api service

ui: example of wiring patchdbmodel into page

begin integration

remove unnecessary method

linting

first data rendering

rework app initialization

http source working for ssh delete call

temp patches working

entire Embassy tab complete

not in kansas anymore

ripping, saving progress

progress for API request response types and endoint defs

Update data-model.ts

shambles, but in a good way

progress

big progress

progress

installed list working

big progress

progress

progress

begin marketplace redesign

Update api-types.ts

Update api-types.ts

marketplace improvements

cosmetic

dependencies and recommendations

begin nym auth approach

install wizard

restore flow and donations
This commit is contained in:
Aaron Greenspan
2021-02-16 13:45:09 -07:00
committed by Aiden McClelland
parent fd685ae32c
commit 594d93eb3b
238 changed files with 15137 additions and 21331 deletions

View File

@@ -1,26 +0,0 @@
<!-- TODO: EJECT-DISKS, add a check box to allow a user to eject a disk on backup completion. -->
<ion-content>
<div style="height: 85%; margin: 20px; display: flex; flex-direction: column; justify-content: space-between;">
<div>
<h4><ion-text color="dark">Ready to Backup</ion-text></h4>
<p><ion-text color="medium">Enter your master password to create an encrypted backup.</ion-text></p>
</div>
<div>
<ion-item lines="none" style="--background: var(--ion-background-color); --border-color: var(--ion-color-medium);">
<ion-label style="font-size: small" position="floating">Master Password</ion-label>
<ion-input style="border-style: solid; border-width: 0px 0px 1px 0px; border-color: var(--ion-color-dark);" [(ngModel)]="password" type="password" (ionChange)="$error$.next('')"></ion-input>
</ion-item>
<ion-item *ngIf="$error$ | async as e" lines="none" style="--background: var(--ion-background-color);">
<ion-label style="font-size: small" color="danger" class="ion-text-wrap">{{e}}</ion-label>
</ion-item>
</div>
<div style="display: flex; justify-content: flex-end; align-items: center;">
<ion-button fill="clear" color="medium" (click)="cancel()">
Cancel
</ion-button>
<ion-button fill="clear" color="primary" (click)="submit()">
Create Backup
</ion-button>
</div>
</div>
</ion-content>

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AppBackupConfirmationComponent } from './app-backup-confirmation.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppBackupConfirmationComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
FormsModule,
],
exports: [AppBackupConfirmationComponent],
})
export class AppBackupConfirmationComponentModule { }

View File

@@ -1,45 +0,0 @@
import { Component, Input, OnInit } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { BehaviorSubject } from 'rxjs'
import { AppInstalledFull } from 'src/app/models/app-types'
import { DiskPartition } from 'src/app/models/server-model'
@Component({
selector: 'app-backup-confirmation',
templateUrl: './app-backup-confirmation.component.html',
styleUrls: ['./app-backup-confirmation.component.scss'],
})
export class AppBackupConfirmationComponent implements OnInit {
unmasked = false
password: string
$error$: BehaviorSubject<string> = new BehaviorSubject('')
// TODO: EJECT-DISKS pass this through the modalCtrl once ejecting disks is an option in the UI.
eject = true
message: string
@Input() app: AppInstalledFull
@Input() partition: DiskPartition
constructor (private readonly modalCtrl: ModalController) { }
ngOnInit () {
this.message = `Enter your master password to create an encrypted backup of ${this.app.title} to "${this.partition.label || this.partition.logicalname}".`
}
toggleMask () {
this.unmasked = !this.unmasked
}
cancel () {
this.modalCtrl.dismiss({ cancel: true })
}
submit () {
if (!this.password || this.password.length < 12) {
this.$error$.next('Password must be at least 12 characters in length.')
return
}
const { password } = this
this.modalCtrl.dismiss({ password })
}
}

View File

@@ -1,12 +1,4 @@
<div style="position: relative; margin-right: 1vh;">
<ion-icon
*ngIf="(badge$ | async) && !(menuFixedOpen$ | async)"
size="medium"
color="dark"
[class.ios-badge]="isIos"
[class.md-badge]="!isIos"
name="alert-outline"
>
</ion-icon>
<ion-badge mode="md" class="md-badge" *ngIf="(badge$ | ngrxPush) && !(menuFixedOpen$ | ngrxPush)" color="danger">{{ badge$ | ngrxPush }}</ion-badge>
<ion-menu-button color="dark"></ion-menu-button>
</div>

View File

@@ -1,17 +1,18 @@
.ios-badge {
background-color: var(--ion-color-start9);
position: absolute;
top: 1px;
left: 62%;
border-radius: 5px;
z-index: 1;
}
// .ios-badge {
// background-color: var(--ion-color-start9);
// position: absolute;
// top: 1px;
// left: 62%;
// border-radius: 5px;
// z-index: 1;
// }
.md-badge {
background-color: var(--ion-color-start9);
position: absolute;
top: -2px;
top: -8px;
left: 56%;
border-radius: 5px;
z-index: 1;
font-size: 80%;
}

View File

@@ -1,9 +1,7 @@
import { Component } from '@angular/core'
import { ServerModel } from '../../models/server-model'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
import { isPlatform } from '@ionic/angular'
import { PatchDbModel } from 'src/app/models/patch-db/patch-db-model'
@Component({
selector: 'badge-menu-button',
@@ -12,16 +10,14 @@ import { isPlatform } from '@ionic/angular'
})
export class BadgeMenuComponent {
badge$: Observable<boolean>
badge$: Observable<number>
menuFixedOpen$: Observable<boolean>
isIos: boolean
constructor (
private readonly serverModel: ServerModel,
private readonly splitPane: SplitPaneTracker,
private readonly patch: PatchDbModel,
) {
this.menuFixedOpen$ = this.splitPane.$menuFixedOpenOnLeft$.asObservable()
this.badge$ = this.serverModel.watch().badge.pipe(map(i => i > 0))
this.isIos = isPlatform('ios')
this.menuFixedOpen$ = this.splitPane.menuFixedOpenOnLeft$.asObservable()
this.badge$ = this.patch.watch$('server-info', 'unread-notification-count')
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { ValueSpec } from 'src/app/app-config/config-types'
import { ValueSpec } from 'src/app/pkg-config/config-types'
@Component({
selector: 'config-header',

View File

@@ -1,14 +0,0 @@
<div *ngFor="let dep of dependenciesToDisplay; let index = i">
<marketplace-dependency-item *ngIf="depType === 'available'" style="width: 100%"
[dep]="dep"
[hostApp]="hostApp"
[$loading$]="$loading$"
>
</marketplace-dependency-item>
<installed-dependency-item *ngIf="depType === 'installed'" style="width: 100%"
[dep]="dep"
[hostApp]="hostApp"
[$loading$]="$loading$"
>
</installed-dependency-item>
</div>

View File

@@ -1,28 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DependencyListComponent } from './dependency-list.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { InformationPopoverComponentModule } from '../information-popover/information-popover.component.module'
import { StatusComponentModule } from '../status/status.component.module'
import { InstalledDependencyItemComponentModule } from './installed-dependency-item/installed-dependency-item.component.module'
import { MarketplaceDependencyItemComponentModule } from './marketplace-dependency-item/marketplace-dependency-item.component.module'
@NgModule({
declarations: [
DependencyListComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
InformationPopoverComponentModule,
StatusComponentModule,
InstalledDependencyItemComponentModule,
MarketplaceDependencyItemComponentModule,
],
exports: [DependencyListComponent],
})
export class DependencyListComponentModule { }

View File

@@ -1,30 +0,0 @@
import { Component, Input } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { AppDependency, BaseApp, isOptional } from 'src/app/models/app-types'
@Component({
selector: 'dependency-list',
templateUrl: './dependency-list.component.html',
styleUrls: ['./dependency-list.component.scss'],
})
export class DependencyListComponent {
@Input() depType: 'installed' | 'available' = 'available'
@Input() hostApp: BaseApp
@Input() dependencies: AppDependency[]
dependenciesToDisplay: AppDependency[]
@Input() $loading$: BehaviorSubject<boolean> = new BehaviorSubject(true)
constructor () { }
ngOnChanges () {
this.dependenciesToDisplay = this.dependencies.filter(dep =>
this.depType === 'available' ? !isOptional(dep) : true,
)
}
ngOnInit () {
this.dependenciesToDisplay = this.dependencies.filter(dep =>
this.depType === 'available' ? !isOptional(dep) : true,
)
}
}

View File

@@ -1,33 +0,0 @@
<ng-container *ngIf="{ loading: $loading$ | async, disabled: installing || ($loading$ | async) } as l" >
<ion-item
class="dependency"
lines="none"
>
<ion-avatar style="position: relative; height: 5vh; width: 5vh; margin: 0px;" slot="start">
<div *ngIf="!l.loading" class="badge" [style]="badgeStyle"></div>
<img [src]="dep.iconURL | iconParse" />
</ion-avatar>
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
<h4 style="font-family: 'Montserrat'">{{ dep.title }}</h4>
<p style="font-size: small">{{ dep.versionSpec }}</p>
<p *ngIf="!l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="color">{{statusText}}</ion-text></p>
<p *ngIf="l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text color="medium">Refreshing</ion-text></p>
</ion-label>
<ion-button size="small" (click)="action()" *ngIf="!installing && !l.loading" color="primary" fill="outline" style="font-size: x-small">
{{actionText}}
</ion-button>
<div slot="end" *ngIf='installing || (l.loading)' >
<div *ngIf='installing && !(l.loading)' class="spinner">
<ion-spinner [color]="color" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
</div>
<div *ngIf='(l.loading)' class="spinner">
<ion-spinner [color]="medium" style="height: 3vh; width: 3vh" name="lines"></ion-spinner>
</div>
</div>
</ion-item>
</ng-container>

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { InstalledDependencyItemComponent } from './installed-dependency-item.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module'
import { StatusComponentModule } from '../../status/status.component.module'
@NgModule({
declarations: [InstalledDependencyItemComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
InformationPopoverComponentModule,
StatusComponentModule,
],
exports: [InstalledDependencyItemComponent],
})
export class InstalledDependencyItemComponentModule { }

View File

@@ -1,30 +0,0 @@
.spinner {
background: rgba(0,0,0,0);
border-radius: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 5px;
}
.badge {
position: absolute; width: 2.5vh;
height: 2.5vh;
border-radius: 50px;
left: -1vh;
top: -1vh;
}
.xSmallText {
font-size: x-small !important;
}
.mediumText {
font-size: medium !important;
}
.opacityUp {
opacity: 0.75;
}

View File

@@ -1,113 +0,0 @@
import { Component, Input, OnInit } from '@angular/core'
import { NavigationExtras } from '@angular/router'
import { AlertController, NavController } from '@ionic/angular'
import { BehaviorSubject, Observable } from 'rxjs'
import { AppStatus } from 'src/app/models/app-model'
import { AppDependency, BaseApp, DependencyViolationSeverity, getInstalledViolationSeverity, getViolationSeverity, isInstalling, isMisconfigured, isMissing, isNotRunning, isVersionMismatch } from 'src/app/models/app-types'
import { Recommendation } from '../../recommendation-button/recommendation-button.component'
@Component({
selector: 'installed-dependency-item',
templateUrl: './installed-dependency-item.component.html',
styleUrls: ['./installed-dependency-item.component.scss'],
})
export class InstalledDependencyItemComponent implements OnInit {
@Input() dep: AppDependency
@Input() hostApp: BaseApp
@Input() $loading$: BehaviorSubject<boolean>
isLoading$: Observable<boolean>
color: string
installing = false
badgeStyle: string
violationSeverity: DependencyViolationSeverity
statusText: string
actionText: string
action: () => Promise<any>
constructor (private readonly navCtrl: NavController, private readonly alertCtrl: AlertController) { }
ngOnInit () {
this.violationSeverity = getInstalledViolationSeverity(this.dep)
const { color, statusText, installing, actionText, action } = this.getValues()
this.color = color
this.statusText = statusText
this.installing = installing
this.actionText = actionText
this.action = action
this.badgeStyle = `background: radial-gradient(var(--ion-color-${this.color}) 40%, transparent)`
}
isDanger () {
// installed dep violations are either REQUIRED or NONE, by getInstalledViolationSeverity above.
return [DependencyViolationSeverity.REQUIRED].includes(this.violationSeverity)
}
getValues (): { color: string, statusText: string, installing: boolean, actionText: string, action: () => Promise<any> } {
if (isInstalling(this.dep)) return { color: 'primary', statusText: 'Installing', installing: true, actionText: undefined, action: () => this.view() }
if (!this.isDanger()) return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View', action: () => this.view() }
if (isMissing(this.dep)) return { color: 'warning', statusText: 'Not Installed', installing: false, actionText: 'Install', action: () => this.install() }
if (isVersionMismatch(this.dep)) return { color: 'warning', statusText: 'Incompatible Version Installed', installing: false, actionText: 'Update', action: () => this.install() }
if (isMisconfigured(this.dep)) return { color: 'warning', statusText: 'Incompatible Config', installing: false, actionText: 'Configure', action: () => this.configure() }
if (isNotRunning(this.dep)) return { color: 'warning', statusText: 'Not Running', installing: false, actionText: 'View', action: () => this.view() }
return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View', action: () => this.view() }
}
async view () {
return this.navCtrl.navigateForward(`/services/installed/${this.dep.id}`)
}
async install () {
const verb = 'requires'
const description = `${this.hostApp.title} ${verb} an install of ${this.dep.title} satisfying ${this.dep.versionSpec}.`
const whyDependency = this.dep.description
const installationRecommendation: Recommendation = {
iconURL: this.hostApp.iconURL,
appId: this.hostApp.id,
description,
title: this.hostApp.title,
versionSpec: this.dep.versionSpec,
whyDependency,
}
const navigationExtras: NavigationExtras = {
state: { installationRecommendation },
}
return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`, navigationExtras)
}
async configure () {
if (this.dep.violation.name !== 'incompatible-config') return
const configViolationDesc = this.dep.violation.ruleViolations
const configViolationFormatted =
`<ul>${configViolationDesc.map(d => `<li>${d}</li>`).join('\n')}</ul>`
const configRecommendation: Recommendation = {
iconURL: this.hostApp.iconURL,
appId: this.hostApp.id,
description: configViolationFormatted,
title: this.hostApp.title,
}
const navigationExtras: NavigationExtras = {
state: { configRecommendation },
}
return this.navCtrl.navigateForward(`/services/installed/${this.dep.id}/config`, navigationExtras)
}
async presentAlertDescription() {
const description = `<p>${this.dep.description}<\p>`
const alert = await this.alertCtrl.create({
backdropDismiss: true,
message: description,
})
await alert.present()
}
}

View File

@@ -1,45 +0,0 @@
<ng-container *ngIf="{ loading: $loading$ | async, disabled: installing || ($loading$ | async) } as l" >
<ion-item
class="dependency"
style="--border-color: var(--ion-color-medium-shade)"
[lines]="presentAlertDescription ? 'inset' : 'full'"
>
<ion-avatar style="position: relative; height: 5vh; width: 5vh; margin: 0px;" slot="start">
<div *ngIf="!l.loading" class="badge" [style]="badgeStyle"></div>
<img [src]="dep.iconURL | iconParse" />
</ion-avatar>
<ion-label class="ion-text-wrap" style="padding: 1vh; padding-left: 2vh">
<h4 style="font-family: 'Montserrat'">{{ dep.title }}
<span *ngIf="recommended" style="font-family: 'Open Sans'; font-size: small; color: var(--ion-color-medium)">(recommended)</span>
</h4>
<p style="font-size: small">{{ dep.versionSpec }}</p>
<p *ngIf="!l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text [color]="color">{{statusText}}</ion-text></p>
<p *ngIf="l.loading" style="padding-top: 2px; position: relative; font-style: italic; font-size: smaller"><ion-text color="medium">Refreshing</ion-text></p>
</ion-label>
<ion-button size="small" (click)="presentAlertDescription=!presentAlertDescription" [disabled]="l.loading" color="medium" fill="clear" style="margin: 14px; font-size: small">
<ion-icon *ngIf="!presentAlertDescription" name="chevron-down"></ion-icon>
<ion-icon *ngIf="presentAlertDescription" name="chevron-up"></ion-icon>
</ion-button>
<ion-button size="small" (click)="toInstall()" *ngIf="!installing && !l.loading" color="primary" fill="outline" style="font-size: small">
{{actionText}}
</ion-button>
<div slot="end" *ngIf='installing || (l.loading)' style="margin: 0" >
<div *ngIf='installing && !(l.loading)' class="spinner">
<ion-spinner [color]="color" style="height: 3vh; width: 3vh" name="dots"></ion-spinner>
</div>
<div *ngIf='(l.loading)' class="spinner">
<ion-spinner [color]="medium" style="height: 3vh; width: 3vh" name="lines"></ion-spinner>
</div>
</div>
</ion-item>
<ion-item style="margin-bottom: 10px"*ngIf="presentAlertDescription" lines="none">
<div style="font-size: small; color: var(--ion-color-medium)" [innerHtml]="descriptionText"></div>
</ion-item>
<div style="height: 8px"></div>
</ng-container>

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { MarketplaceDependencyItemComponent } from './marketplace-dependency-item.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module'
import { StatusComponentModule } from '../../status/status.component.module'
@NgModule({
declarations: [MarketplaceDependencyItemComponent],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
InformationPopoverComponentModule,
StatusComponentModule,
],
exports: [MarketplaceDependencyItemComponent],
})
export class MarketplaceDependencyItemComponentModule { }

View File

@@ -1,35 +0,0 @@
.spinner {
background: rgba(0,0,0,0);
border-radius: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 14px;
}
.badge {
position: absolute; width: 2.5vh;
height: 2.5vh;
border-radius: 50px;
left: -1vh;
top: -1vh;
}
.xSmallText {
font-size: x-small !important;
}
.mediumText {
font-size: medium !important;
}
.opacityUp {
opacity: 0.75;
}
.dependency {
--padding-start: 20px;
--padding-end: 2px;
}

View File

@@ -1,88 +0,0 @@
import { Component, Input, OnInit } from '@angular/core'
import { NavigationExtras } from '@angular/router'
import { NavController } from '@ionic/angular'
import { BehaviorSubject, Observable } from 'rxjs'
import { AppDependency, BaseApp, DependencyViolationSeverity, getViolationSeverity, isOptional, isMissing, isInstalling, isRecommended, isVersionMismatch } from 'src/app/models/app-types'
import { Recommendation } from '../../recommendation-button/recommendation-button.component'
@Component({
selector: 'marketplace-dependency-item',
templateUrl: './marketplace-dependency-item.component.html',
styleUrls: ['./marketplace-dependency-item.component.scss'],
})
export class MarketplaceDependencyItemComponent implements OnInit {
@Input() dep: AppDependency
@Input() hostApp: BaseApp
@Input() $loading$: BehaviorSubject<boolean>
presentAlertDescription = false
isLoading$: Observable<boolean>
color: string
installing = false
recommended = false
badgeStyle: string
violationSeverity: DependencyViolationSeverity
statusText: string
actionText: 'View' | 'Get'
descriptionText: string
constructor (
private readonly navCtrl: NavController,
) { }
ngOnInit () {
this.violationSeverity = getViolationSeverity(this.dep)
if (isOptional(this.dep)) throw new Error('Do not display optional deps, satisfied or otherwise, on the AAL')
const { actionText, color, statusText, installing } = this.getValues()
this.color = color
this.statusText = statusText
this.installing = installing
this.recommended = isRecommended(this.dep)
this.actionText = actionText
this.badgeStyle = `background: radial-gradient(var(--ion-color-${this.color}) 40%, transparent)`
this.descriptionText = `<p>${this.dep.description}<\p>`
if (this.recommended) {
this.descriptionText = this.descriptionText + `<p>This service is not required: ${this.dep.optional}<\p>`
}
}
isDanger (): boolean {
return [DependencyViolationSeverity.REQUIRED, DependencyViolationSeverity.RECOMMENDED].includes(this.violationSeverity)
}
getValues (): { color: string, statusText: string, installing: boolean, actionText: 'View' | 'Get' } {
if (isInstalling(this.dep)) return { color: 'primary', statusText: 'Installing', installing: true, actionText: undefined }
if (!this.isDanger()) return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View' }
if (isMissing(this.dep)) return { color: 'warning', statusText: 'Not Installed', installing: false, actionText: 'Get' }
if (isVersionMismatch(this.dep)) return { color: 'warning', statusText: 'Incompatible Version Installed', installing: false, actionText: 'Get' }
return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View' }
}
async toInstall () {
if (this.actionText === 'View') return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`)
const verb = this.violationSeverity === DependencyViolationSeverity.REQUIRED ? 'requires' : 'recommends'
const description = `${this.hostApp.title} ${verb} an install of ${this.dep.title} satisfying ${this.dep.versionSpec}.`
const whyDependency = this.dep.description
const installationRecommendation: Recommendation = {
iconURL: this.hostApp.iconURL,
appId: this.hostApp.id,
description,
title: this.hostApp.title,
versionSpec: this.dep.versionSpec,
whyDependency,
}
const navigationExtras: NavigationExtras = {
state: { installationRecommendation },
}
return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`, navigationExtras)
}
}

View File

@@ -1,6 +0,0 @@
<ion-item lines="none" *ngIf="$error$ | async as error" class="notifier-item" style="margin-top: 12px">
<ion-label class="ion-text-wrap" color="danger"><p>{{ error }}</p></ion-label>
<ion-button *ngIf="dismissable" size="small" slot="end" fill="outline" color="danger" (click)="clear()">
<ion-icon style="height: 12px; width: 12px;" name="close"></ion-icon>
</ion-button>
</ion-item>

View File

@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ErrorMessageComponent } from './error-message.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({
declarations: [
ErrorMessageComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
],
exports: [ErrorMessageComponent],
})
export class ErrorMessageComponentModule { }

View File

@@ -1,10 +0,0 @@
.error-message {
--background: var(--ion-color-danger);
margin: 12px;
border-radius: 3px;
font-weight: bold;
}
.legacy-error-message {
margin: 5px;
}

View File

@@ -1,19 +0,0 @@
import { Component, Input, OnInit } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
@Component({
selector: 'error-message',
templateUrl: './error-message.component.html',
styleUrls: ['./error-message.component.scss'],
})
export class ErrorMessageComponent implements OnInit {
@Input() $error$: BehaviorSubject<string | undefined> = new BehaviorSubject(undefined)
@Input() dismissable = true
constructor () { }
ngOnInit () { }
clear () {
this.$error$.next(undefined)
}
}

View File

@@ -1,7 +1,7 @@
<div *ngIf="!($loading$ | async) && !params.skipCompletionDialogue" class="slide-content">
<div *ngIf="!(loading$ | ngrxPush) && !params.skipCompletionDialogue" class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
<ion-label [color]="$color$ | ngrxPush" style="font-size: xx-large; font-weight: bold;">
{{successText}}
</ion-label>
</div>
@@ -11,7 +11,7 @@
</div>
</div>
<div *ngIf="$loading$ | async" class="center-spinner">
<div *ngIf="loading$ | ngrxPush" class="center-spinner">
<ion-spinner color="warning" name="lines"></ion-spinner>
<ion-label class="long-message">{{label}}</ion-label>
</div>

View File

@@ -28,17 +28,18 @@ export class CompleteComponent implements OnInit, Loadable {
}
$loading$ = new BehaviorSubject(false)
$color$ = new BehaviorSubject('medium')
$cancel$ = new Subject<void>()
loading$ = new BehaviorSubject(false)
color$ = new BehaviorSubject('medium')
cancel$ = new Subject<void>()
label: string
summary: string
successText: string
load () {
markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe(
{ error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
markAsLoadingDuring$(this.loading$, from(this.params.executeAction())).pipe(takeUntil(this.cancel$)).subscribe(
{
error: e => this.transitions.error(new Error(`${this.params.action} failed: ${e.message || e}`)),
complete: () => this.params.skipCompletionDialogue && this.transitions.final(),
},
)
@@ -50,37 +51,37 @@ export class CompleteComponent implements OnInit, Loadable {
case 'install':
this.summary = `Installation of ${this.params.title} is now in progress. You will receive a notification when the installation has completed.`
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
this.$color$.next('primary')
this.color$.next('primary')
this.successText = 'In Progress'
break
case 'downgrade':
this.summary = `Downgrade for ${this.params.title} is now in progress. You will receive a notification when the downgrade has completed.`
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
this.$color$.next('primary')
this.color$.next('primary')
this.successText = 'In Progress'
break
case 'update':
this.summary = `Update for ${this.params.title} is now in progress. You will receive a notification when the update has completed.`
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
this.$color$.next('primary')
this.color$.next('primary')
this.successText = 'In Progress'
break
case 'uninstall':
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully uninstalled.`
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
this.$color$.next('success')
this.color$.next('success')
this.successText = 'Success'
break
case 'stop':
this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully stopped.`
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
this.$color$.next('success')
this.color$.next('success')
this.successText = 'Success'
break
case 'configure':
this.summary = `New config for ${this.params.title} has been successfully saved.`
this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...`
this.$color$.next('success')
this.color$.next('success')
this.successText = 'Success'
break
}

View File

@@ -1,31 +0,0 @@
<div class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
{{label}}
</ion-label>
</div>
<div class="long-message">
{{longMessage}}
</div>
<div style="margin: 25px 0px;">
<ion-item
style="--ion-item-background: rgb(0,0,0,0); margin-top: 5px"
*ngFor="let dep of dependencyViolations"
>
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
<div class="badge" [style]="dep.badgeStyle"></div>
<img [src]="dep.iconURL | iconParse" />
</ion-avatar>
<ion-label>
<h5>{{dep.title}}</h5>
<ion-text color="medium" style="font-size: smaller">{{dep.versionSpec}}</ion-text>
</ion-label>
<ion-text [color]="dep.color" style="font-size: smaller; font-style: italic; margin-right: 5px;">{{dep.violation}}</ion-text>
<status *ngIf="dep.isInstalling" [appStatus]="'INSTALLING'" size="italics-small"></status>
</ion-item>
</div>
</div>
</div>

View File

@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DependenciesComponent } from './dependencies.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { StatusComponentModule } from '../../status/status.component.module'
@NgModule({
declarations: [
DependenciesComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule.forChild([]),
SharingModule,
StatusComponentModule,
],
exports: [DependenciesComponent],
})
export class DependenciesComponentModule { }

View File

@@ -1,127 +0,0 @@
import { Component, Input, OnInit } from '@angular/core'
import { PopoverController } from '@ionic/angular'
import { BehaviorSubject, Subject } from 'rxjs'
import { AppStatus } from 'src/app/models/app-model'
import { AppDependency, DependencyViolationSeverity, getViolationSeverity } from 'src/app/models/app-types'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { InformationPopoverComponent } from '../../information-popover/information-popover.component'
import { Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({
selector: 'dependencies',
templateUrl: './dependencies.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class DependenciesComponent implements OnInit, Loadable {
@Input() params: {
action: WizardAction,
title: string,
version: string,
serviceRequirements: AppDependency[]
}
filteredServiceRequirements: AppDependency[]
$loading$ = new BehaviorSubject(false)
$cancel$ = new Subject<void>()
longMessage: string
dependencyViolations: {
iconURL: string
title: string,
versionSpec: string,
violation: string,
color: string,
badgeStyle: string
}[]
label: string
$color$ = new BehaviorSubject('medium')
constructor (private readonly popoverController: PopoverController) { }
load () {
this.$color$.next(this.$color$.getValue())
}
ngOnInit () {
this.filteredServiceRequirements = this.params.serviceRequirements.filter(dep => {
return [DependencyViolationSeverity.REQUIRED, DependencyViolationSeverity.RECOMMENDED].includes(getViolationSeverity(dep))
})
.filter(dep => ['incompatible-version', 'missing'].includes(dep.violation.name))
this.dependencyViolations = this.filteredServiceRequirements
.map(dep => ({
iconURL: dep.iconURL,
title: dep.title,
versionSpec: (dep.violation && dep.violation.name === 'incompatible-config' && 'reconfigure') || dep.versionSpec,
isInstalling: dep.violation && dep.violation.name === 'incompatible-status' && dep.violation.status === AppStatus.INSTALLING,
violation: renderViolation(dep),
color: 'medium',
badgeStyle: `background: radial-gradient(var(--ion-color-warning) 40%, transparent)`,
}))
this.setSeverityAttributes()
}
setSeverityAttributes () {
switch (getWorstViolationSeverity(this.filteredServiceRequirements)){
case DependencyViolationSeverity.REQUIRED:
this.longMessage = `${this.params.title} requires the installation of other services. Don't worry, you'll be able to install these requirements later.`
this.label = 'Notice'
this.$color$.next('dark')
break
case DependencyViolationSeverity.RECOMMENDED:
this.longMessage = `${this.params.title} recommends the installation of other services. Don't worry, you'll be able to install these requirements later.`
this.label = 'Notice'
this.$color$.next('dark')
break
default:
this.longMessage = `All installation requirements for ${this.params.title} version ${displayEmver(this.params.version)} are met.`
this.$color$.next('success')
this.label = `Ready`
}
}
async presentPopover (ev: any, information: string) {
const popover = await this.popoverController.create({
component: InformationPopoverComponent,
event: ev,
translucent: false,
showBackdrop: true,
backdropDismiss: true,
componentProps: {
information,
},
})
return popover.present()
}
}
function renderViolation1 (dep: AppDependency): string {
const severity = getViolationSeverity(dep)
switch (severity){
case DependencyViolationSeverity.REQUIRED: return 'mandatory'
case DependencyViolationSeverity.RECOMMENDED: return 'recommended'
case DependencyViolationSeverity.OPTIONAL: return 'optional'
case DependencyViolationSeverity.NONE: return 'none'
}
}
function renderViolation (dep: AppDependency): string {
const severity = renderViolation1(dep)
if (severity === 'none') return ''
switch (dep.violation.name){
case 'missing': return `${severity}`
case 'incompatible-version': return `${severity}`
case 'incompatible-config': return ``
case 'incompatible-status': return ''
default: return ''
}
}
function getWorstViolationSeverity (rs: AppDependency[]) : DependencyViolationSeverity {
if (!rs) return DependencyViolationSeverity.NONE
return rs.map(getViolationSeverity).sort( (a, b) => b - a )[0] || DependencyViolationSeverity.NONE
}

View File

@@ -1,5 +1,5 @@
<div>
<div *ngIf="!($loading$ | async)" class="slide-content">
<div *ngIf="!(loading$ | ngrxPush)" class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label color="warning" style="font-size: xx-large; font-weight: bold;"
@@ -26,7 +26,7 @@
*ngFor="let dep of dependentBreakages"
>
<ion-avatar style="position: relative; height: 4vh; width: 4vh" slot="start">
<img [src]="dep.iconURL | iconParse" />
<img [src]="dep.iconURL" />
</ion-avatar>
<ion-label>
<h5>{{dep.title}}</h5>
@@ -35,7 +35,7 @@
</div>
</div>
</div>
<div *ngIf="$loading$ | async" class="center-spinner">
<div *ngIf="loading$ | ngrxPush" class="center-spinner">
<ion-spinner color="warning" name="lines"></ion-spinner>
<ion-label class="long-message">Checking for installed services which depend on {{params.title}}...</ion-label>
</div>

View File

@@ -1,9 +1,9 @@
import { Component, Input, OnInit } from '@angular/core'
import { BehaviorSubject, from, Subject } from 'rxjs'
import { takeUntil, tap } from 'rxjs/operators'
import { DependentBreakage } from 'src/app/models/app-types'
import { Breakages } from 'src/app/services/api/api-types'
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { capitalizeFirstLetter, isEmptyObject } from 'src/app/util/misc.util'
import { Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@@ -17,7 +17,7 @@ export class DependentsComponent implements OnInit, Loadable {
title: string,
action: WizardAction, //Are you sure you want to *uninstall*...,
verb: string, // *Uninstalling* will cause problems...
fetchBreakages: () => Promise<DependentBreakage[]>,
fetchBreakages: () => Promise<Breakages>,
skipConfirmationDialogue?: boolean
}
@Input() transitions: {
@@ -27,34 +27,33 @@ export class DependentsComponent implements OnInit, Loadable {
error: (e: Error) => void
}
dependentBreakages: DependentBreakage[]
dependentBreakages: Breakages
hasDependentViolation: boolean
longMessage: string | null = null
$color$ = new BehaviorSubject('medium') // this will display disabled while loading
$loading$ = new BehaviorSubject(false)
$cancel$ = new Subject<void>()
color$ = new BehaviorSubject('medium') // this will display disabled while loading
loading$ = new BehaviorSubject(false)
cancel$ = new Subject<void>()
constructor () { }
ngOnInit () { }
load () {
this.$color$.next('medium')
markAsLoadingDuring$(this.$loading$, from(this.params.fetchBreakages())).pipe(
takeUntil(this.$cancel$),
tap(breakages => this.dependentBreakages = breakages || []),
this.color$.next('medium')
markAsLoadingDuring$(this.loading$, from(this.params.fetchBreakages())).pipe(
takeUntil(this.cancel$),
tap(breakages => this.dependentBreakages = breakages),
).subscribe(
{
complete: () => {
this.hasDependentViolation = this.dependentBreakages && this.dependentBreakages.length > 0
this.hasDependentViolation = this.dependentBreakages && !isEmptyObject(this.dependentBreakages)
if (this.hasDependentViolation) {
this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.`
this.$color$.next('warning')
this.color$.next('warning')
} else if (this.params.skipConfirmationDialogue) {
this.transitions.next()
} else {
this.longMessage = `No other services installed on your Embassy will be affected by this action.`
this.$color$.next('success')
this.color$.next('success')
}
},
error: (e: Error) => this.transitions.error(new Error(`Fetching dependent service information failed: ${e.message || e}`)),

View File

@@ -8,18 +8,18 @@
</ion-header>
<ion-content>
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slides *ngIf="!(error$ | ngrxPush)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slide *ngFor="let def of params.slideDefinitions">
<!-- We can pass [transitions]="transitions" into the component if logic within the component needs to trigger a transition (not just bottom bar) -->
<dependencies #components *ngIf="def.slide.selector === 'dependencies'" [params]="def.slide.params"></dependencies>
<notes #components *ngIf="def.slide.selector === 'notes'" [params]="def.slide.params"></notes>
<notes #components *ngIf="def.slide.selector === 'notes'" [params]="def.slide.params" style="width: 100%;"></notes>
<dependents #components *ngIf="def.slide.selector === 'dependents'" [params]="def.slide.params" [transitions]="transitions"></dependents>
<complete #components *ngIf="def.slide.selector === 'complete'" [params]="def.slide.params" [transitions]="transitions"></complete>
</ion-slide>
</ion-slides>
<div *ngIf="$error$ | async as error" class="slide-content">
<div *ngIf="error$ | ngrxPush as error" class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label color="danger" style="font-size: xx-large; font-weight: bold;">
@@ -35,7 +35,7 @@
<ion-footer>
<ion-toolbar style="padding: 8px;">
<ng-container *ngIf="!($initializing$ | async) && !($error$ | async) && { loading: currentSlide.$loading$ | async, bar: currentBottomBar} as v">
<ng-container *ngIf="!(initializing$ | ngrxPush) && !(error$ | ngrxPush) && { loading: currentSlideloading$ | ngrxPush, bar: currentBottomBar} as v">
<!-- cancel button if loading/not loading -->
<ion-button slot="start" *ngIf="v.loading && v.bar.cancel.whileLoading as cancel" (click)="transitions.cancel()" class="toolbar-button" fill="outline" color="medium">
@@ -58,7 +58,7 @@
</ion-button>
</ng-container>
<ng-container *ngIf="$error$ | async">
<ng-container *ngIf="error$ | ngrxPush">
<ion-button slot="start" (click)="transitions.final()" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
</ng-container>
</ion-toolbar>

View File

@@ -4,7 +4,6 @@ import { InstallWizardComponent } from './install-wizard.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module'
import { NotesComponentModule } from './notes/notes.component.module'
@@ -18,7 +17,6 @@ import { NotesComponentModule } from './notes/notes.component.module'
IonicModule,
RouterModule.forChild([]),
SharingModule,
DependenciesComponentModule,
DependentsComponentModule,
CompleteComponentModule,
NotesComponentModule,

View File

@@ -1,10 +1,8 @@
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { Component, Input, NgZone, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
import { BehaviorSubject } from 'rxjs'
import { Cleanup } from 'src/app/util/cleanup'
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
import { CompleteComponent } from './complete/complete.component'
import { DependenciesComponent } from './dependencies/dependencies.component'
import { DependentsComponent } from './dependents/dependents.component'
import { NotesComponent } from './notes/notes.component'
import { Loadable } from './loadable'
@@ -15,7 +13,7 @@ import { WizardAction } from './wizard-types'
templateUrl: './install-wizard.component.html',
styleUrls: ['./install-wizard.component.scss'],
})
export class InstallWizardComponent extends Cleanup implements OnInit {
export class InstallWizardComponent {
@Input() params: {
// defines each slide along with bottom bar
slideDefinitions: SlideDefinition[]
@@ -40,11 +38,13 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
return this.params.slideDefinitions[this.slideIndex].bottomBar
}
$initializing$ = new BehaviorSubject(true)
$error$ = new BehaviorSubject(undefined)
initializing$ = new BehaviorSubject(true)
error$ = new BehaviorSubject(undefined)
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
ngOnInit () { }
constructor (
private readonly modalController: ModalController,
private readonly zone: NgZone,
) { }
ngAfterViewInit () {
this.currentSlide.load()
@@ -53,15 +53,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
}
ionViewDidEnter () {
this.$initializing$.next(false)
this.initializing$.next(false)
}
// process bottom bar buttons
private transition = (info: { next: any } | { error: Error } | { cancelled: true } | { final: true }) => {
const i = info as { next?: any, error?: Error, cancelled?: true, final?: true }
if (i.cancelled) this.currentSlide.$cancel$.next()
if (i.cancelled) this.currentSlide.cancel$.next()
if (i.final || i.cancelled) return this.modalController.dismiss(i)
if (i.error) return this.$error$.next(capitalizeFirstLetter(i.error.message))
if (i.error) return this.error$.next(capitalizeFirstLetter(i.error.message))
this.moveToNextSlide(i.next)
}
@@ -90,7 +90,6 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
export interface SlideDefinition {
slide:
{ selector: 'dependencies', params: DependenciesComponent['params'] } |
{ selector: 'dependents', params: DependentsComponent['params'] } |
{ selector: 'complete', params: CompleteComponent['params'] } |
{ selector: 'notes', params: NotesComponent['params'] }

View File

@@ -3,7 +3,7 @@ import { BehaviorSubject, Subject } from 'rxjs'
export interface Loadable {
load: (prevResult?: any) => void
result?: any // fill this variable on slide 1 to get passed into the load on slide 2. If this variable is falsey, it will skip the next slide.
$loading$: BehaviorSubject<boolean> // will be true during load function
$cancel$: Subject<void> // will cancel load function
loading$: BehaviorSubject<boolean> // will be true during load function
cancel$: Subject<void> // will cancel load function
}

View File

@@ -14,8 +14,8 @@ export class NotesComponent implements OnInit, Loadable {
titleColor: string
}
$loading$ = new BehaviorSubject(false)
$cancel$ = new Subject<void>()
loading$ = new BehaviorSubject(false)
cancel$ = new Subject<void>()
load () { }

View File

@@ -1,8 +1,7 @@
import { Injectable } from '@angular/core'
import { AppModel, AppStatus } from 'src/app/models/app-model'
import { OsUpdateService } from 'src/app/services/os-update.service'
import { InstalledPackageDataEntry } from 'src/app/models/patch-db/data-model'
import { Breakages } from 'src/app/services/api/api-types'
import { exists } from 'src/app/util/misc.util'
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
import { ApiService } from '../../services/api/api.service'
import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component'
import { WizardAction } from './wizard-types'
@@ -11,43 +10,34 @@ import { WizardAction } from './wizard-types'
export class WizardBaker {
constructor (
private readonly apiService: ApiService,
private readonly updateService: OsUpdateService,
private readonly appModel: AppModel,
) { }
install (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
id: string, title: string, version: string, installAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version, serviceRequirements, installAlert } = values
const { id, title, version, installAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
validate(version, exists, 'missing version')
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
const action = 'install'
const toolbar: TopbarParams = { action, title, version }
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? {
slide: {
selector: 'notes',
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
params: {
notes: installAlert,
title: 'Warning',
titleColor: 'warning',
},
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next',
},
} : undefined,
{
slide: {
selector: 'dependencies',
params: { action, title, version, serviceRequirements },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Install',
},
},
{
slide: {
selector: 'complete',
@@ -55,7 +45,7 @@ export class WizardBaker {
action,
verb: 'beginning installation for',
title,
executeAction: () => this.apiService.installApp(id, version).then(app => { this.appModel.add({ ...app, status: AppStatus.INSTALLING })}),
executeAction: () => this.apiService.installPackage({ id, version }),
},
},
bottomBar: {
@@ -68,14 +58,13 @@ export class WizardBaker {
}
update (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
id: string, title: string, version: string, installAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version, serviceRequirements, installAlert } = values
const { id, title, version, installAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
validate(version, exists, 'missing version')
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
const action = 'update'
const toolbar: TopbarParams = { action, title, version }
@@ -84,26 +73,26 @@ export class WizardBaker {
installAlert ? {
slide: {
selector: 'notes',
params: { notes: installAlert, title: 'Warning', titleColor: 'warning'},
params: {
notes: installAlert,
title: 'Warning',
titleColor: 'warning',
},
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Next',
},
} : undefined,
{ slide: {
selector: 'dependencies',
params: { action, title, version, serviceRequirements },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Update',
},
},
{ slide: {
{
slide: {
selector: 'dependents',
params: {
skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
skipConfirmationDialogue: true,
action,
verb: 'updating',
title,
fetchBreakages: () => this.apiService.dryUpdatePackage({ id, version }).then( ({ breakages }) => breakages ),
},
},
bottomBar: {
@@ -111,12 +100,14 @@ export class WizardBaker {
next: 'Update Anyways',
},
},
{ slide: {
{
slide: {
selector: 'complete',
params: {
action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
}),
action,
verb: 'beginning update for',
title,
executeAction: () => this.apiService.installPackage({ id, version }),
},
},
bottomBar: {
@@ -138,18 +129,27 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ slide : {
{
slide : {
selector: 'notes',
params: { notes: releaseNotes, title: 'Release Notes', titleColor: 'dark' },
params: {
notes: releaseNotes,
title: 'Release Notes',
titleColor: 'dark',
},
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } }, next: 'Update OS',
},
},
{ slide: {
{
slide: {
selector: 'complete',
params: {
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
action,
verb: 'beginning update for',
title,
executeAction: () => this.apiService.updateServer({ }),
},
},
bottomBar: {
@@ -162,14 +162,13 @@ export class WizardBaker {
}
downgrade (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
id: string, title: string, version: string, installAlert?: string
}): InstallWizardComponent['params'] {
const { id, title, version, serviceRequirements, installAlert } = values
const { id, title, version, installAlert } = values
validate(id, exists, 'missing id')
validate(title, exists, 'missing title')
validate(version, exists, 'missing version')
validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements')
const action = 'downgrade'
const toolbar: TopbarParams = { action, title, version }
@@ -178,23 +177,22 @@ export class WizardBaker {
installAlert ? {
slide: {
selector: 'notes',
params: { notes: installAlert, title: 'Warning', titleColor: 'warning' },
params: {
notes: installAlert,
title: 'Warning',
titleColor: 'warning',
},
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Next' },
} : undefined,
{ slide: {
selector: 'dependencies',
params: { action, title, version, serviceRequirements },
},
bottomBar: {
cancel: { afterLoading: { text: 'Cancel' } },
next: 'Downgrade',
},
},
{ slide: {
selector: 'dependents',
params: {
skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ),
skipConfirmationDialogue: true,
action,
verb: 'downgrading',
title,
fetchBreakages: () => this.apiService.dryUpdatePackage({ id, version }).then( ({ breakages }) => breakages ),
},
},
bottomBar: {
@@ -204,9 +202,10 @@ export class WizardBaker {
{ slide: {
selector: 'complete',
params: {
action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => {
this.appModel.update({ id: app.id, status: AppStatus.INSTALLING })
}),
action,
verb: 'beginning downgrade for',
title,
executeAction: () => this.apiService.installPackage({ id, version }),
},
},
bottomBar: {
@@ -231,7 +230,8 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ slide: {
{
slide: {
selector: 'notes',
params: {
notes: uninstallAlert || defaultUninstallationWarning(title),
@@ -241,18 +241,26 @@ export class WizardBaker {
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Continue' },
},
{ slide: {
{
slide: {
selector: 'dependents',
params: {
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
action,
verb: 'uninstalling',
title,
fetchBreakages: () => this.apiService.dryRemovePackage({ id }).then( ({ breakages }) => breakages ),
},
},
bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' },
},
{ slide: {
{
slide: {
selector: 'complete',
params: {
action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)),
action,
verb: 'uninstalling',
title,
executeAction: () => this.apiService.removePackage({ id }),
},
},
bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } },
@@ -262,7 +270,7 @@ export class WizardBaker {
}
stop (values: {
breakages: DependentBreakage[], id: string, title: string, version: string
breakages: Breakages, id: string, title: string, version: string
}): InstallWizardComponent['params'] {
const { breakages, title, version } = values
@@ -274,10 +282,14 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ slide: {
{
slide: {
selector: 'dependents',
params: {
action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages),
action,
verb: 'stopping',
title,
fetchBreakages: () => Promise.resolve(breakages),
},
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Stop Anyways' },
@@ -286,19 +298,20 @@ export class WizardBaker {
return { toolbar, slideDefinitions }
}
configure (values: {
breakages: DependentBreakage[], app: AppInstalledPreview
}): InstallWizardComponent['params'] {
const { breakages, app } = values
const { title, versionInstalled: version } = app
configure (values: { breakages: Breakages, pkg: InstalledPackageDataEntry }): InstallWizardComponent['params'] {
const { breakages, pkg } = values
const { title, version } = pkg.manifest
const action = 'configure'
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ slide: {
{
slide: {
selector: 'dependents',
params: {
action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages),
action,
verb: 'saving config for',
title, fetchBreakages: () => Promise.resolve(breakages),
},
},
bottomBar: { cancel: { afterLoading: { text: 'Cancel' } }, next: 'Save Config Anyways' },

View File

@@ -1,9 +1,9 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { Annotation, Annotations } from '../../app-config/config-utilities'
import { Annotation, Annotations } from '../../pkg-config/config-utilities'
import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service'
import { ConfigCursor } from 'src/app/app-config/config-cursor'
import { ModalPresentable } from 'src/app/app-config/modal-presentable'
import { ValueSpecOf, ValueSpec } from 'src/app/app-config/config-types'
import { ConfigCursor } from 'src/app/pkg-config/config-cursor'
import { ModalPresentable } from 'src/app/pkg-config/modal-presentable'
import { ValueSpecOf, ValueSpec } from 'src/app/pkg-config/config-types'
import { MaskPipe } from 'src/app/pipes/mask.pipe'
@Component({

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core'
import { PwaBackService } from 'src/app/services/pwa-back.service'
import { NavController } from '@ionic/angular'
@Component({
selector: 'pwa-back-button',
templateUrl: './pwa-back.component.html',
@@ -7,10 +8,10 @@ import { PwaBackService } from 'src/app/services/pwa-back.service'
})
export class PwaBackComponent {
constructor (
private readonly pwaBack: PwaBackService,
private readonly nav: NavController,
) { }
navigateBack () {
return this.pwaBack.back()
return this.nav.back()
}
}

View File

@@ -4,6 +4,6 @@
border-color: #FFEB3B;
border-width: medium;
box-shadow: 0 0 10px white;" size="small" (click)="presentPopover($event)">
<img [src]="rec.iconURL | iconParse" />
<img [src]="rec.iconURL" />
</ion-fab-button>
</ion-fab>

View File

@@ -1,8 +1,7 @@
import { Component, Input, OnInit } from '@angular/core'
import { Component, Input } from '@angular/core'
import { Router } from '@angular/router'
import { PopoverController } from '@ionic/angular'
import { filter, take } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { InformationPopoverComponent } from '../information-popover/information-popover.component'
@@ -11,12 +10,13 @@ import { InformationPopoverComponent } from '../information-popover/information-
templateUrl: './recommendation-button.component.html',
styleUrls: ['./recommendation-button.component.scss'],
})
export class RecommendationButtonComponent extends Cleanup implements OnInit {
export class RecommendationButtonComponent {
@Input() rec: Recommendation
@Input() raise?: { id: string }
constructor (private readonly router: Router, private readonly popoverController: PopoverController) {
super()
}
constructor (
private readonly router: Router,
private readonly popoverController: PopoverController,
) { }
ngOnInit () {
if (!this.raise) return
@@ -41,7 +41,7 @@ export class RecommendationButtonComponent extends Cleanup implements OnInit {
componentProps: {
information: `
<div style="font-size: medium; font-style: italic; margin: 5px 0px;">
${capitalizeFirstLetter(this.rec.title)} Installation Recommendations
${capitalizeFirstLetter(this.rec.dependentTitle)} Installation Recommendations
</div>
<div>
${this.rec.description}
@@ -57,10 +57,9 @@ export class RecommendationButtonComponent extends Cleanup implements OnInit {
}
export type Recommendation = {
title: string
appId: string
iconURL: string,
description: string,
versionSpec?: string
whyDependency?: string
dependentId: string
dependentTitle: string
dependentIcon: string,
description: string
version?: string
}

View File

@@ -1,24 +1,24 @@
<p *ngIf="size === 'small'" style="margin: 0 0 4px 0;">
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
<ion-text [color]="color">{{ display }}</ion-text>
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
</p>
<h3 *ngIf="size === 'italics-small'" style="margin: 0 0 4px 0;">
<ion-text [style]="style" style="font-size: small; font-style: italic; text-transform: lowercase;" [color]="color">{{ display }}</ion-text>
<p *ngIf="size === 'italics-small'" style="margin: 0 0 4px 0; font-style: italic;">
<ion-text [color]="color">{{ display }}</ion-text>
<ion-spinner *ngIf="showDots" class="dots dots-small" name="dots" [color]="color"></ion-spinner>
</h3>
</p>
<h3 *ngIf="size === 'medium'">
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
<ion-text [color]="color">{{ display }}</ion-text>
<ion-spinner *ngIf="showDots" class="dots dots-medium" name="dots" [color]="color"></ion-spinner>
</h3>
<h1 *ngIf="size === 'large'">
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
<ion-text [color]="color">{{ display }}</ion-text>
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
</h1>
<h1 style="font-size: 18px; font-weight: 500" *ngIf="size === 'bold-large'">
<ion-text [style]="style" [color]="color">{{ display }}</ion-text>
<h1 *ngIf="size === 'bold-large'" style="font-size: 18px; font-weight: 500">
<ion-text [color]="color">{{ display }}</ion-text>
<ion-spinner *ngIf="showDots" class="dots" name="dots" [color]="color"></ion-spinner>
</h1>

View File

@@ -15,18 +15,3 @@
height: 24px;
padding-left: 12px;
}
.dots {
vertical-align: middle;
margin-left: 8px;
}
.dots-small {
width: 12px !important;
height: 12px !important;
}
.dots-medium {
width: 16px !important;
height: 16px !important;
}

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { AppStatus } from 'src/app/models/app-model'
import { ServerStatus } from 'src/app/models/server-model'
import { ServerStatusRendering, AppStatusRendering } from '../../util/status-rendering'
import { PackageDataEntry } from 'src/app/models/patch-db/data-model'
import { ConnectionState } from 'src/app/services/connection.service'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
@Component({
selector: 'status',
@@ -9,48 +9,18 @@ import { ServerStatusRendering, AppStatusRendering } from '../../util/status-ren
styleUrls: ['./status.component.scss'],
})
export class StatusComponent {
@Input() appStatus?: AppStatus
@Input() serverStatus?: ServerStatus
@Input() pkg: PackageDataEntry
@Input() connection: ConnectionState
@Input() size: 'small' | 'medium' | 'large' | 'italics-small' | 'bold-large' = 'large'
@Input() text: string = ''
color: string
display: string
showDots: boolean
style = ''
display = ''
color = ''
showDots = false
ngOnChanges () {
if (this.serverStatus) {
this.handleServerStatus()
} else if (this.appStatus) {
this.handleAppStatus()
}
}
handleServerStatus () {
let res = ServerStatusRendering[this.serverStatus]
if (!res) {
console.warn(`Received invalid server status from the server: `, this.serverStatus)
res = ServerStatusRendering[ServerStatus.UNKNOWN]
}
const { display, color, showDots } = res
const { display, color, showDots } = renderPkgStatus(this.pkg, this.connection)
this.display = display
this.color = color
this.showDots = showDots
}
handleAppStatus () {
let res = AppStatusRendering[this.appStatus]
if (!res) {
console.warn(`Received invalid app status from the server: `, this.appStatus)
res = AppStatusRendering[AppStatus.UNKNOWN]
}
const { display, color, showDots, style } = res
this.display = display + this.text
this.color = color
this.showDots = showDots
this.style = style
}
}

View File

@@ -1,5 +0,0 @@
<ion-item button lines="none" *ngIf="updateAvailable$ | async as res" (click)="confirmUpdate(res)">
<ion-label>
New EmbassyOS Version {{res.versionLatest | displayEmver}} Available!
</ion-label>
</ion-item>

View File

@@ -1,18 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { UpdateOsBannerComponent } from './update-os-banner.component'
import { IonicModule } from '@ionic/angular'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({
declarations: [
UpdateOsBannerComponent,
],
imports: [
CommonModule,
IonicModule,
SharingModule,
],
exports: [UpdateOsBannerComponent],
})
export class UpdateOsBannerComponentModule { }

View File

@@ -1,11 +0,0 @@
ion-item {
--background: linear-gradient(90deg, var(--ion-color-light), var(--ion-color-primary));
--min-height: 0px;
ion-label {
font-family: 'Open Sans';
font-size: small;
text-align: center;
font-weight: bold;
}
}

View File

@@ -1,35 +0,0 @@
import { Component } from '@angular/core'
import { OsUpdateService } from 'src/app/services/os-update.service'
import { Observable } from 'rxjs'
import { ModalController } from '@ionic/angular'
import { WizardBaker } from '../install-wizard/prebaked-wizards'
import { wizardModal } from '../install-wizard/install-wizard.component'
import { ReqRes } from 'src/app/services/api/api.service'
@Component({
selector: 'update-os-banner',
templateUrl: './update-os-banner.component.html',
styleUrls: ['./update-os-banner.component.scss'],
})
export class UpdateOsBannerComponent {
updateAvailable$: Observable<undefined | ReqRes.GetVersionLatestRes>
constructor (
private readonly osUpdateService: OsUpdateService,
private readonly modalCtrl: ModalController,
private readonly wizardBaker: WizardBaker,
) {
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
}
ngOnInit () { }
async confirmUpdate (res: ReqRes.GetVersionLatestRes) {
await wizardModal(
this.modalCtrl,
this.wizardBaker.updateOS({
version: res.versionLatest,
releaseNotes: res.releaseNotes,
}),
)
}
}