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

@@ -88,7 +88,10 @@ export class FilterPackagesPipe implements PipeTransform {
return packages
.filter(p => category === 'all' || p.categories.includes(category))
.sort((a, b) => {
return a['published-at'] > b['published-at'] ? -1 : 1
return (
new Date(b['published-at']).valueOf() -
new Date(a['published-at']).valueOf()
)
})
}
}

View File

@@ -41,7 +41,6 @@
<ion-note></ion-note>
<ion-progress-bar></ion-progress-bar>
<ion-radio></ion-radio>
<ion-reorder></ion-reorder>
<ion-row></ion-row>
<ion-searchbar></ion-searchbar>
<ion-segment></ion-segment>

View File

@@ -71,7 +71,6 @@ const ICONS = [
'remove',
'remove-circle-outline',
'remove-outline',
'reorder-three',
'rocket-outline',
'save-outline',
'shield-checkmark-outline',

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() {

View File

@@ -749,7 +749,7 @@ export module Mock {
icon: PROXY_ICON,
},
},
'published-at': new Date().toISOString(),
'published-at': new Date(new Date().valueOf() + 10).toISOString(),
},
},
'btc-rpc-proxy': {

View File

@@ -223,9 +223,6 @@ export module RR {
export type UninstallPackageReq = { id: string } // package.uninstall
export type UninstallPackageRes = null
export type DeleteRecoveredPackageReq = { id: string } // package.delete-recovered
export type DeleteRecoveredPackageRes = null
export type DryConfigureDependencyReq = {
'dependency-id': string
'dependent-id': string

View File

@@ -216,10 +216,6 @@ export abstract class ApiService {
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes>
abstract deleteRecoveredPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes>
abstract sideloadPackage(
params: RR.SideloadPackageReq,
): Promise<RR.SideloadPacakgeRes>

View File

@@ -364,12 +364,6 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.stop', params })
}
async deleteRecoveredPackage(
params: RR.DeleteRecoveredPackageReq,
): Promise<RR.DeleteRecoveredPackageRes> {
return this.rpcRequest({ method: 'package.delete-recovered', params })
}
async uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {

View File

@@ -814,19 +814,6 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async deleteRecoveredPackage(
params: RR.DeleteRecoveredPackageReq,
): Promise<RR.DeleteRecoveredPackageRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/recovered-packages/${params.id}`,
},
]
return this.withRevision(patch)
}
async dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes> {
@@ -892,10 +879,6 @@ export class MockApiService extends ApiService {
op: PatchOp.REMOVE,
path: `/package-data/${id}/install-progress`,
},
{
op: PatchOp.REMOVE,
path: `/recovered-packages/${id}`,
},
]
this.mockRevision(patch2)
}, 1000)

View File

@@ -50,13 +50,6 @@ export const mockPatchData: DataModel = {
},
hostname: 'random-words',
},
'recovered-packages': {
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
version: '0.2.2',
},
},
'package-data': {
bitcoind: {
state: PackageState.Installed,

View File

@@ -6,7 +6,6 @@ import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-in
export interface DataModel {
'server-info': ServerInfo
'package-data': { [id: string]: PackageDataEntry }
'recovered-packages': { [id: string]: RecoveredPackageDataEntry }
ui: UIData
}
@@ -75,11 +74,6 @@ export enum ServerStatus {
Updated = 'updated',
BackingUp = 'backing-up',
}
export interface RecoveredPackageDataEntry {
title: string
icon: Url
version: string
}
export interface PackageDataEntry {
state: PackageState

View File

@@ -1,50 +0,0 @@
import {
DataModel,
PackageDataEntry,
RecoveredPackageDataEntry,
} from 'src/app/services/patch-db/data-model'
export function parseDataModel(data: DataModel): ParsedData {
const all: Record<string, PackageDataEntry> = JSON.parse(
JSON.stringify(data['package-data']),
)
// recovered packages (0.2.x)
const recoveredPkgs = Object.entries(data['recovered-packages'])
.filter(([id, _]) => !all[id])
.map(([id, val]) => ({
...val,
id,
}))
// installed packages
const order = [...(data.ui['pkg-order'] || [])]
const pkgs: PackageDataEntry[] = []
// add known packages in preferential order
order.forEach(id => {
if (all[id]) {
pkgs.push(all[id])
delete all[id]
}
})
// unshift unknown packages
Object.values(all).forEach(pkg => {
pkgs.unshift(pkg)
})
return {
pkgs,
recoveredPkgs,
}
}
export interface RecoveredInfo extends RecoveredPackageDataEntry {
id: string
}
interface ParsedData {
pkgs: PackageDataEntry[]
recoveredPkgs: RecoveredInfo[]
}