Progress bar (#377)

* looking for unnecessary watches

* on init data initialization

* prog pipe

* install progress everywhere

* titlecase status

* include updateing state in app show for install progress

Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Drew Ansbacher
2021-07-22 16:14:57 -06:00
committed by Aiden McClelland
parent 09fa589ae5
commit 11da141b73
17 changed files with 120 additions and 50 deletions

View File

@@ -22,7 +22,7 @@ export class MaintenanceGuard implements CanActivate, CanActivateChild {
} }
private runServerStatusCheck (): boolean { private runServerStatusCheck (): boolean {
if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(this.patch.data['server-info']?.status)) { if ([ServerStatus.Updating, ServerStatus.BackingUp].includes(this.patch.getData()['server-info']?.status)) {
this.router.navigate(['/maintenance'], { replaceUrl: true }) this.router.navigate(['/maintenance'], { replaceUrl: true })
return false return false
} else { } else {

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { CanActivate, Router } from '@angular/router' import { CanActivate, Router } from '@angular/router'
import { tap } from 'rxjs/operators'
import { ServerStatus } from '../services/patch-db/data-model' import { ServerStatus } from '../services/patch-db/data-model'
import { PatchDbService } from '../services/patch-db/patch-db.service' import { PatchDbService } from '../services/patch-db/patch-db.service'

View File

@@ -9,6 +9,7 @@ import { MaskPipe } from '../pipes/mask.pipe'
import { HasUiPipe, LaunchablePipe } from '../pipes/ui.pipe' import { HasUiPipe, LaunchablePipe } from '../pipes/ui.pipe'
import { EmptyPipe } from '../pipes/empty.pipe' import { EmptyPipe } from '../pipes/empty.pipe'
import { NotificationColorPipe } from '../pipes/notification-color.pipe' import { NotificationColorPipe } from '../pipes/notification-color.pipe'
import { InstallState } from '../pipes/install-state.pipe'
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -16,6 +17,7 @@ import { NotificationColorPipe } from '../pipes/notification-color.pipe'
EmverSatisfiesPipe, EmverSatisfiesPipe,
TypeofPipe, TypeofPipe,
IncludesPipe, IncludesPipe,
InstallState,
MarkdownPipe, MarkdownPipe,
AnnotationStatusPipe, AnnotationStatusPipe,
TruncateCenterPipe, TruncateCenterPipe,
@@ -40,6 +42,7 @@ import { NotificationColorPipe } from '../pipes/notification-color.pipe'
MaskPipe, MaskPipe,
EmverDisplayPipe, EmverDisplayPipe,
HasUiPipe, HasUiPipe,
InstallState,
LaunchablePipe, LaunchablePipe,
EmptyPipe, EmptyPipe,
NotificationColorPipe, NotificationColorPipe,

View File

@@ -26,7 +26,7 @@ export class AppInstructionsPage {
async ngOnInit () { async ngOnInit () {
const pkgId = this.route.snapshot.paramMap.get('pkgId') const pkgId = this.route.snapshot.paramMap.get('pkgId')
const url = this.patch.data['package-data'][pkgId]['static-files'].instructions const url = this.patch.getData()['package-data'][pkgId]['static-files'].instructions
try { try {
this.instructions = await this.embassyApi.getStatic(url) this.instructions = await this.embassyApi.getStatic(url)

View File

@@ -36,7 +36,8 @@
<img *ngIf="!connectionFailure" [class]="pkg.value.bulb.class" [src]="pkg.value.bulb.img" /> <img *ngIf="!connectionFailure" [class]="pkg.value.bulb.class" [src]="pkg.value.bulb.img" />
<ion-card-header> <ion-card-header>
<status [rendering]="pkg.value.statusRendering" size="calc(8px + .4vw)" weight="bold"></status> <status *ngIf="[PackageState.Installed, PackageState.Removing] | includes : pkg.value.entry.state" [rendering]="pkg.value.statusRendering" size="calc(8px + .4vw)" weight="bold"></status>
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.value.entry.state" class="installing-status"><ion-text color="primary">{{ pkg.value.entry.state | titlecase }}...{{ (pkg.value.entry['install-progress'] | installState).totalProgress }}%</ion-text></p>
<ion-card-title>{{ pkg.value.entry.manifest.title }}</ion-card-title> <ion-card-title>{{ pkg.value.entry.manifest.title }}</ion-card-title>
</ion-card-header> </ion-card-header>
</ion-card> </ion-card>

View File

@@ -78,3 +78,9 @@
position: absolute; position: absolute;
right: 0px; right: 0px;
} }
.installing-status {
font-size: calc(8px + .4vw);
font-weight: bold;
margin: 0;
}

View File

@@ -2,10 +2,10 @@ import { Component } from '@angular/core'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service' import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry, PackageState } from 'src/app/services/patch-db/data-model'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { PkgStatusRendering, renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import { distinctUntilChanged, filter } from 'rxjs/operators' import { filter } from 'rxjs/operators'
@Component({ @Component({
selector: 'app-list', selector: 'app-list',
@@ -24,6 +24,7 @@ export class AppListPage {
statusRendering: PkgStatusRendering | null statusRendering: PkgStatusRendering | null
sub: Subscription | null sub: Subscription | null
}} = { } }} = { }
PackageState = PackageState
constructor ( constructor (
private readonly config: ConfigService, private readonly config: ConfigService,
@@ -41,7 +42,6 @@ export class AppListPage {
) )
.subscribe(pkgs => { .subscribe(pkgs => {
const ids = Object.keys(pkgs) const ids = Object.keys(pkgs)
console.log('PKGSPKGS', ids)
Object.keys(this.pkgs).forEach(id => { Object.keys(this.pkgs).forEach(id => {
if (!ids.includes(id)) { if (!ids.includes(id)) {

View File

@@ -42,12 +42,35 @@
</ion-button> </ion-button>
</div> </div>
<ion-button size="small" *ngIf="pkg | hasUi" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()"> <ng-container *ngIf="pkg.state === PackageState.Installed">
<ion-button size="small" *ngIf="(pkg | hasUi)" [disabled]="!(pkg | isLaunchable)" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon> <ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button> </ion-button>
</ng-container>
</div> </div>
<div *ngIf="[PackageState.Installing, PackageState.Updating] | includes : pkg.state" style="padding: 16px;">
<p>Downloading: {{ (pkg['install-progress'] | installState).downloadProgress }}%</p>
<ion-progress-bar
[color]="pkg['install-progress']['download-complete'] ? 'success' : 'secondary'"
[value]="(pkg['install-progress'] | installState).downloadProgress / 100"
></ion-progress-bar>
<p>Validating: {{ (pkg['install-progress'] | installState).validateProgress }}%</p>
<ion-progress-bar
[color]="pkg['install-progress']['validation-complete'] ? 'success' : 'secondary'"
[value]="(pkg['install-progress'] | installState).validateProgress / 100"
></ion-progress-bar>
<p>Installing: {{ (pkg['install-progress'] | installState).unpackProgress }}%</p>
<ion-progress-bar
[color]="pkg['install-progress']['unpack-complete'] ? 'success' : 'secondary'"
[value]="(pkg['install-progress'] | installState).unpackProgress / 100"
></ion-progress-bar>
</div>
<ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : rendering.feStatus)"> <ng-container *ngIf="!([FeStatus.Installing, FeStatus.Updating, FeStatus.Removing] | includes : rendering.feStatus)">
<ion-grid class="ion-text-center" style="margin: 0 6px;"> <ion-grid class="ion-text-center" style="margin: 0 6px;">
<ion-row> <ion-row>

View File

@@ -28,6 +28,7 @@ export class AppShowPage {
PackageState = PackageState PackageState = PackageState
DependencyErrorType = DependencyErrorType DependencyErrorType = DependencyErrorType
rendering: PkgStatusRendering rendering: PkgStatusRendering
Math = Math
@ViewChild(IonContent) content: IonContent @ViewChild(IonContent) content: IonContent
subs: Subscription[] = [] subs: Subscription[] = []

View File

@@ -58,17 +58,12 @@
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0" color="success">Installed</ion-text> <ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 0" color="success">Installed</ion-text>
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text> <ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text>
</p> </p>
<p *ngIf="localPkg.state === PackageState.Installing" style="display: flex; flex-direction: row; align-items: center;"> <p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
<ion-text color="primary">Installing</ion-text> <ion-text color="primary">{{ localPkg.state | titlecase }}...{{ (localPkg['install-progress'] | installState).totalProgress }}%</ion-text>
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner>
</p> </p>
<p *ngIf="localPkg.state === PackageState.Updating" style="display: flex; flex-direction: row; align-items: center;"> <p *ngIf="localPkg.state === PackageState.Removing">
<ion-text color="primary">Updating</ion-text> <ion-text color="warning">{{ localPkg.state | Removing }}</ion-text>
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="primary"></ion-spinner> <ion-spinner name="dots" color="warning"></ion-spinner>
</p>
<p *ngIf="localPkg.state === PackageState.Removing" style="display: flex; flex-direction: row; align-items: center;">
<ion-text color="danger">Removing</ion-text>
<ion-spinner name="crescent" style="height: 10px; width: 15px; margin-left: 3px; margin-right: -4px;" color="danger"></ion-spinner>
</p> </p>
</ng-container> </ng-container>
</ion-label> </ion-label>

View File

@@ -25,41 +25,43 @@
<h1 class="header-title">{{ pkg.manifest.title }}</h1> <h1 class="header-title">{{ pkg.manifest.title }}</h1>
<p class="header-version">{{ pkg.manifest.version | displayEmver }}</p> <p class="header-version">{{ pkg.manifest.version | displayEmver }}</p>
<div class="header-status"> <div class="header-status">
<!-- no installedPkg --> <!-- no localPkg -->
<p *ngIf="!installedPkg; else local"> <p *ngIf="!localPkg; else local">
<ion-text color="medium">Not Installed</ion-text> <ion-text color="medium">Not Installed</ion-text>
</p> </p>
<!-- installedPkg --> <!-- localPkg -->
<ng-template #local> <ng-template #local>
<p *ngIf="installedPkg.state !== PackageState.Installed; else installed">
<!-- installing, updating, removing -->
<ion-text [color]="installedPkg.state === PackageState.Removing ? 'danger' : 'primary'">{{ installedPkg.state }}</ion-text>
<ion-spinner class="dots dots-medium" name="dots" [color]="installedPkg.state === PackageState.Removing ? 'danger' : 'primary'"></ion-spinner>
</p>
<!-- installed --> <!-- installed -->
<ng-template #installed> <p *ngIf="localPkg.state === PackageState.Installed">
<p> <ion-text color="medium">Installed at {{ localPkg.manifest.version | displayEmver }}</ion-text>
<ion-text color="medium">Installed at {{ installedPkg.manifest.version | displayEmver }}</ion-text> </p>
<!-- installing, updating -->
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
<ion-text color="primary">{{ localPkg.state | titlecase }}...{{ (localPkg['install-progress'] | installState).totalProgress }}%</ion-text>
</p>
<!-- removing -->
<p *ngIf="localPkg.state === PackageState.Removing">
<ion-text color="warning">{{ localPkg.state | titlecase }}</ion-text>
<ion-spinner name="dots" color="warning"></ion-spinner>
</p> </p>
</ng-template>
</ng-template> </ng-template>
</div> </div>
</div> </div>
</div> </div>
</ion-col> </ion-col>
<ion-col sizeXl="3" sizeLg="3" sizeMd="3" sizeSm="12" sizeXs="12" class="ion-align-self-center"> <ion-col sizeXl="3" sizeLg="3" sizeMd="3" sizeSm="12" sizeXs="12" class="ion-align-self-center">
<!-- no installedPkg --> <!-- no localPkg -->
<ion-button *ngIf="!installedPkg; else installedPkg2" class="main-action-button" expand="block" (click)="install()"> <ion-button *ngIf="!localPkg; else localPkg2" class="main-action-button" expand="block" (click)="install()">
Install Install
</ion-button> </ion-button>
<!-- installedPkg --> <!-- localPkg -->
<ng-template #installedPkg2> <ng-template #localPkg2>
<!-- not installing, updating, or removing --> <!-- not installing, updating, or removing -->
<ng-container *ngIf="installedPkg.state === PackageState.Installed"> <ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button *ngIf="(installedPkg.manifest.version | compareEmver : pkg.manifest.version) === -1" class="main-action-button" expand="block" (click)="update('update')"> <ion-button *ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === -1" class="main-action-button" expand="block" (click)="update('update')">
Update Update
</ion-button> </ion-button>
<ion-button *ngIf="(installedPkg.manifest.version | compareEmver : pkg.manifest.version) === 1" class="main-action-button" expand="block" color="warning" (click)="update('downgrade')"> <ion-button *ngIf="(localPkg.manifest.version | compareEmver : pkg.manifest.version) === 1" class="main-action-button" expand="block" color="warning" (click)="update('downgrade')">
Downgrade Downgrade
</ion-button> </ion-button>
</ng-container> </ng-container>

View File

@@ -21,7 +21,7 @@
.header-status { .header-status {
p { p {
margin: 0; margin: 0;
font-size: calc(6px + 1vw) font-size: calc(16px + 1vw)
} }
} }
} }

View File

@@ -22,7 +22,7 @@ export class MarketplaceShowPage {
@ViewChild(IonContent) content: IonContent @ViewChild(IonContent) content: IonContent
loading = true loading = true
pkgId: string pkgId: string
installedPkg: PackageDataEntry localPkg: PackageDataEntry
PackageState = PackageState PackageState = PackageState
rec: Recommendation | null = null rec: Recommendation | null = null
showRec = true showRec = true
@@ -48,7 +48,7 @@ export class MarketplaceShowPage {
this.subs = [ this.subs = [
this.patch.watch$('package-data', this.pkgId) this.patch.watch$('package-data', this.pkgId)
.subscribe(pkg => { .subscribe(pkg => {
this.installedPkg = pkg this.localPkg = pkg
}), }),
] ]

View File

@@ -0,0 +1,39 @@
import { Pipe, PipeTransform } from '@angular/core'
import { InstallProgress } from '../services/patch-db/data-model'
@Pipe({
name: 'installState',
})
export class InstallState implements PipeTransform {
transform (loadData: InstallProgress): ProgressData {
const { downloaded, validated, unpacked, size } = loadData
const downloadWeight = 1
const validateWeight = .2
const unpackWeight = .7
const numerator = Math.floor(
downloadWeight * downloaded +
validateWeight * validated +
unpackWeight * unpacked)
const denominator = Math.floor(loadData.size * (downloadWeight + validateWeight + unpackWeight))
return {
totalProgress: Math.round(100 * numerator / denominator),
downloadProgress: Math.round(100 * downloaded / size),
validateProgress: Math.round(100 * validated / size),
unpackProgress: Math.round(100 * unpacked / size),
isComplete: loadData['download-complete'] && loadData['validation-complete'] && loadData['unpack-complete'],
}
}
}
export interface ProgressData {
totalProgress: number
downloadProgress: number
validateProgress: number
unpackProgress: number
isComplete: boolean
}

View File

@@ -485,12 +485,11 @@ export class MockApiService extends ApiService {
] ]
for (let phase of phases) { for (let phase of phases) {
let i = initialProgress[phase.progress] let i = initialProgress[phase.progress]
console.log('PHASE', phase)
console.log('Initial i', i) console.log('Initial i', i)
while (i < initialProgress.size) { while (i < initialProgress.size) {
console.log(i) console.log(i)
await pauseFor(1000) await pauseFor(1000)
i = Math.min(i + 40, initialProgress.size) i = Math.min(i + 5, initialProgress.size)
initialProgress[phase.progress] = i initialProgress[phase.progress] = i
if (i === initialProgress.size) { if (i === initialProgress.size) {
initialProgress[phase.completion] = true initialProgress[phase.completion] = true

View File

@@ -85,7 +85,7 @@ export class MarketplaceMockApiService extends MarketplaceApiService {
if (this.useLocal(url)) { if (this.useLocal(url)) {
await pauseFor(2000) await pauseFor(2000)
return params.ids.reduce((obj, id) => { return params.ids.reduce((obj, id) => {
obj[id] = this.patch.data['package-data']?.[id]?.manifest.version.replace('0', '1') obj[id] = this.patch.getData()['package-data']?.[id]?.manifest.version.replace('0', '1')
return obj return obj
}, { }) }, { })
} }

View File

@@ -22,8 +22,9 @@ export class PatchDbService {
connectionStatus$ = new BehaviorSubject(ConnectionStatus.Initializing) connectionStatus$ = new BehaviorSubject(ConnectionStatus.Initializing)
private patchDb: PatchDB<DataModel> private patchDb: PatchDB<DataModel>
private patchSub: Subscription private patchSub: Subscription
data: DataModel
get data () { return this.patchDb.store.cache.data } getData () { return this.patchDb.store.cache.data }
constructor ( constructor (
@Inject(PATCH_SOURCE) private readonly source: Source<DataModel>, @Inject(PATCH_SOURCE) private readonly source: Source<DataModel>,
@@ -34,6 +35,7 @@ export class PatchDbService {
async init (): Promise<void> { async init (): Promise<void> {
const cache = await this.bootstrapper.init() const cache = await this.bootstrapper.init()
this.patchDb = new PatchDB([this.source, this.http], this.http, cache) this.patchDb = new PatchDB([this.source, this.http], this.http, cache)
this.data = this.patchDb.store.cache.data
} }
start (): void { start (): void {