remove recovered services and drop reordering feature (#1829)

This commit is contained in:
Matt Hill
2022-09-26 13:37:41 -06:00
committed by Aiden McClelland
parent 35b220d7a5
commit 2ddd38796d
21 changed files with 35 additions and 440 deletions

View File

@@ -1,22 +0,0 @@
<ion-item>
<ion-thumbnail slot="start">
<img alt="" [src]="rec.icon" />
</ion-thumbnail>
<ion-label>
<h2>{{ rec.title }}</h2>
<p>{{ rec.version | displayEmver }}</p>
</ion-label>
<ion-spinner *ngIf="loading$ | async; else actions"></ion-spinner>
<ng-template #actions>
<div slot="end">
<ion-button fill="clear" color="danger" (click)="deleteRecovered(rec)">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
<!-- Remove -->
</ion-button>
<ion-button fill="clear" color="success" (click)="install$.next(rec)">
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
<!-- Install -->
</ion-button>
</div>
</ng-template>
</ion-item>

View File

@@ -1,110 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Inject,
Input,
Output,
} from '@angular/core'
import { AlertController } from '@ionic/angular'
import { ErrorToastService } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { from, merge, OperatorFunction, pipe, Subject } from 'rxjs'
import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators'
import { RecoveredInfo } from 'src/app/util/parse-data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Component({
selector: 'app-list-rec',
templateUrl: 'app-list-rec.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppListRecComponent {
// Asynchronous actions initiators
readonly install$ = new Subject<RecoveredInfo>()
readonly delete$ = new Subject<RecoveredInfo>()
@Input()
rec!: RecoveredInfo
@Output()
readonly deleted = new EventEmitter<void>()
// Installing package
readonly installing$ = this.install$.pipe(
switchMap(({ id, version }) =>
// Mapping each installation to API request
this.marketplaceService
.installPackage({
id,
'version-spec': `>=${version}`,
'version-priority': 'min',
})
.pipe(
// Mapping operation to true/false loading indication
loading(this.errToast),
),
),
)
// Deleting package
readonly deleting$ = this.delete$.pipe(
switchMap(({ id }) =>
// Mapping each deletion to API request
from(this.api.deleteRecoveredPackage({ id })).pipe(
// Notifying parent component that package is removed from recovered items
tap(() => this.deleted.emit()),
// Mapping operation to true/false loading indication
loading(this.errToast),
),
),
)
// Merging both true/false loading indicators to a single stream
readonly loading$ = merge(this.installing$, this.deleting$)
constructor(
private readonly api: ApiService,
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
) {}
async deleteRecovered(pkg: RecoveredInfo): Promise<void> {
const alert = await this.alertCtrl.create({
header: 'Delete Data',
message: `This action will permanently delete all data associated with ${pkg.title}.`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Delete',
handler: () => {
// Initiate deleting of 'pkg'
this.delete$.next(pkg)
},
cssClass: 'enter-click',
},
],
})
await alert.present()
}
}
// Custom RxJS operator to turn asynchronous operation into a true/false loading indicator
function loading(
errToast: ErrorToastService,
): OperatorFunction<unknown, boolean> {
return pipe(
// Show notification on error
catchError(e => from(errToast.present(e))),
// Map any result to false to stop loading indicator
map(() => false),
// Start operation with true
startWith(true),
)
}

View File

@@ -1,63 +0,0 @@
<!-- header -->
<ion-item-divider class="ion-padding-bottom">
{{ reordering ? 'Reorder' : 'Installed Services' }}
<ion-button
*ngIf="pkgs.length > 1"
slot="end"
fill="clear"
(click)="toggle()"
strong
>
<ion-icon
slot="start"
[name]="reordering ? 'checkmark' : 'swap-vertical'"
></ion-icon>
{{ reordering ? 'Done' : 'Reorder' }}
</ion-button>
</ion-item-divider>
<!-- reordering -->
<ion-list *ngIf="reordering; else grid">
<ion-reorder-group disabled="false" (ionItemReorder)="reorder($any($event))">
<ng-container *ngFor="let item of pkgs">
<ion-item
color="light"
*ngIf="item | packageInfo | async as pkg"
class="item"
>
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
<ion-thumbnail slot="start">
<img alt="" [src]="pkg.entry['static-files'].icon" />
</ion-thumbnail>
<ion-label>
<h2>{{ pkg.entry.manifest.title }}</h2>
<p>{{ pkg.entry.manifest.version | displayEmver }}</p>
<status
[rendering]="pkg.primaryRendering"
[installProgress]="pkg.entry['install-progress']"
weight="bold"
size="small"
[sigtermTimeout]="pkg.entry.manifest.main['sigterm-timeout']"
></status>
</ion-label>
<ion-reorder slot="end">
<ion-icon name="reorder-three" size="large"></ion-icon>
</ion-reorder>
</ion-item>
</ng-container>
</ion-reorder-group>
</ion-list>
<!-- not reordering -->
<ng-template #grid>
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of pkgs" sizeXs="12" sizeXl="6">
<app-list-pkg
*ngIf="pkg | packageInfo | async as info"
[pkg]="info"
></app-list-pkg>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>

View File

@@ -1,39 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
import { ItemReorderEventDetail } from '@ionic/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@Component({
selector: 'app-list-reorder',
templateUrl: 'app-list-reorder.component.html',
styleUrls: ['app-list-reorder.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppListReorderComponent {
@Input()
reordering = false
@Input()
pkgs: readonly PackageDataEntry[] = []
@Output()
readonly reorderingChange = new EventEmitter<boolean>()
@Output()
readonly pkgsChange = new EventEmitter<readonly PackageDataEntry[]>()
toggle() {
this.reordering = !this.reordering
this.reorderingChange.emit(this.reordering)
}
reorder({ detail }: CustomEvent<ItemReorderEventDetail>): void {
this.pkgs = detail.complete([...this.pkgs])
this.pkgsChange.emit(this.pkgs)
}
}

View File

@@ -14,8 +14,6 @@ import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
import { AppListIconComponent } from './app-list-icon/app-list-icon.component'
import { AppListEmptyComponent } from './app-list-empty/app-list-empty.component'
import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component'
import { AppListRecComponent } from './app-list-rec/app-list-rec.component'
import { AppListReorderComponent } from './app-list-reorder/app-list-reorder.component'
import { PackageInfoPipe } from './package-info.pipe'
const routes: Routes = [
@@ -42,8 +40,6 @@ const routes: Routes = [
AppListIconComponent,
AppListEmptyComponent,
AppListPkgComponent,
AppListRecComponent,
AppListReorderComponent,
PackageInfoPipe,
],
})

View File

@@ -21,23 +21,20 @@
></app-list-empty>
<ng-template #list>
<app-list-reorder
*ngIf="pkgs.length"
[(pkgs)]="pkgs"
[reordering]="reordering"
(reorderingChange)="onReordering($event)"
></app-list-reorder>
<ion-item-divider class="ion-padding-bottom"
>Installed Services</ion-item-divider
>
<ng-container *ngIf="recoveredPkgs.length && !reordering">
<ion-item-group>
<ion-item-divider>Recovered Services</ion-item-divider>
<app-list-rec
*ngFor="let rec of recoveredPkgs"
[rec]="rec"
(deleted)="deleteRecovered(rec)"
></app-list-rec>
</ion-item-group>
</ng-container>
<ion-grid>
<ion-row>
<ion-col *ngFor="let pkg of pkgs" sizeSm="12" sizeLg="6">
<app-list-pkg
*ngIf="pkg | packageInfo | async as info"
[pkg]="info"
></app-list-pkg>
</ion-col>
</ion-row>
</ion-grid>
</ng-template>
</ng-template>
</ion-content>

View File

@@ -1,3 +0,0 @@
ion-item-divider {
margin-bottom: 16px;
}

View File

@@ -4,11 +4,9 @@ import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { Observable } from 'rxjs'
import { filter, map, switchMap, take, takeUntil, tap } from 'rxjs/operators'
import { isEmptyObject, exists, DestroyService } from '@start9labs/shared'
import { filter, takeUntil, tap } from 'rxjs/operators'
import { DestroyService } from '@start9labs/shared'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { parseDataModel, RecoveredInfo } from 'src/app/util/parse-data-model'
@Component({
selector: 'app-list',
@@ -19,8 +17,6 @@ import { parseDataModel, RecoveredInfo } from 'src/app/util/parse-data-model'
export class AppListPage {
loading = true
pkgs: readonly PackageDataEntry[] = []
recoveredPkgs: readonly RecoveredInfo[] = []
reordering = false
constructor(
private readonly api: ApiService,
@@ -29,62 +25,22 @@ export class AppListPage {
) {}
get empty(): boolean {
return !this.pkgs.length && isEmptyObject(this.recoveredPkgs)
return !this.pkgs.length
}
ngOnInit() {
this.patch
.watch$()
.watch$('package-data')
.pipe(
filter(data => exists(data) && !isEmptyObject(data)),
take(1),
map(parseDataModel),
tap(({ pkgs, recoveredPkgs }) => {
filter(pkgs => Object.keys(pkgs).length !== this.pkgs.length),
tap(pkgs => {
this.loading = false
this.pkgs = pkgs
this.recoveredPkgs = recoveredPkgs
this.pkgs = Object.values(pkgs).sort((a, b) =>
b.manifest.title > a.manifest.title ? -1 : 1,
)
}),
switchMap(() => this.watchNewlyRecovered()),
takeUntil(this.destroy$),
)
.subscribe()
}
onReordering(reordering: boolean): void {
if (!reordering) {
this.setOrder()
}
this.reordering = reordering
}
deleteRecovered(rec: RecoveredInfo): void {
this.recoveredPkgs = this.recoveredPkgs.filter(item => item !== rec)
}
private watchNewlyRecovered(): Observable<unknown> {
return this.patch.watch$('package-data').pipe(
filter(pkgs => !!pkgs && Object.keys(pkgs).length !== this.pkgs.length),
tap(pkgs => {
const ids = Object.keys(pkgs)
const newIds = ids.filter(
id => !this.pkgs.find(pkg => pkg.manifest.id === id),
)
// remove uninstalled
const filtered = this.pkgs.filter(pkg => ids.includes(pkg.manifest.id))
// add new entry to beginning of array
const added = newIds.map(id => pkgs[id])
this.pkgs = [...added, ...filtered]
this.recoveredPkgs = this.recoveredPkgs.filter(rec => !pkgs[rec.id])
}),
)
}
private setOrder(): void {
const order = this.pkgs.map(pkg => pkg.manifest.id)
this.api.setDbValue({ pointer: '/pkg-order', value: order })
}
}

View File

@@ -10,8 +10,7 @@ import { ActivatedRoute } from '@angular/router'
import { PatchDB } from 'patch-db-client'
import { ServerNameService } from 'src/app/services/server-name.service'
import { Observable, of } from 'rxjs'
import { take, tap } from 'rxjs/operators'
import { isEmptyObject, ErrorToastService } from '@start9labs/shared'
import { ErrorToastService } from '@start9labs/shared'
import { EOSService } from 'src/app/services/eos.service'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
@@ -25,7 +24,6 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
styleUrls: ['server-show.page.scss'],
})
export class ServerShowPage {
hasRecoveredPackage = false
clicks = 0
readonly server$ = this.patch.watch$('server-info')
@@ -48,34 +46,14 @@ export class ServerShowPage {
private readonly authService: AuthService,
) {}
ngOnInit() {
this.patch
.watch$('recovered-packages')
.pipe(
take(1),
tap(data => (this.hasRecoveredPackage = !isEmptyObject(data))),
)
.subscribe()
}
async updateEos(): Promise<void> {
if (this.hasRecoveredPackage) {
const alert = await this.alertCtrl.create({
header: 'Cannot Update',
message:
'You cannot update embassyOS when you have unresolved recovered services.',
buttons: ['OK'],
})
await alert.present()
} else {
const modal = await this.modalCtrl.create({
componentProps: {
releaseNotes: this.eosService.eos?.['release-notes'],
},
component: OSUpdatePage,
})
modal.present()
}
const modal = await this.modalCtrl.create({
componentProps: {
releaseNotes: this.eosService.eos?.['release-notes'],
},
component: OSUpdatePage,
})
modal.present()
}
async presentAlertLogout() {