mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Feat/update tab (#1865)
* implement updates tab for viewing all updates from all marketplaces in one place * remove auto-check-updates * feat: implement updates page (#1888) * feat: implement updates page * chore: comments * better styling in update tab * rework marketplace service (#1891) * rework marketplace service * remove unneeded ? * fix: refactor marketplace to cache requests Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
committed by
Aiden McClelland
parent
d380cc31fa
commit
26c37ba824
@@ -18,9 +18,9 @@ export class AppListPage {
|
||||
const length = next.length
|
||||
return !length || prev.length !== length
|
||||
}),
|
||||
map(([_, pkgs]) => {
|
||||
return pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1))
|
||||
}),
|
||||
map(([_, pkgs]) =>
|
||||
pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1)),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<skeleton-list *ngIf="loading" [rows]="3"></skeleton-list>
|
||||
<skeleton-list *ngIf="loading"></skeleton-list>
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
|
||||
<ion-label>{{ metric.key }}</ion-label>
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
</ion-row>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col size="12">
|
||||
<ng-container *ngIf="marketplace$ | async as marketplace; else loading">
|
||||
<ng-container *ngIf="store$ | async as store; else loading">
|
||||
<ng-container *ngIf="localPkgs$ | async as localPkgs">
|
||||
<marketplace-categories
|
||||
[categories]="marketplace.categories"
|
||||
[categories]="store.categories"
|
||||
[category]="category"
|
||||
[updatesAvailable]="
|
||||
(marketplace.pkgs | filterPackages: '':'updates':localPkgs).length
|
||||
(store.packages | filterPackages: '':'updates':localPkgs).length
|
||||
"
|
||||
(categoryChange)="onCategoryChange($event)"
|
||||
></marketplace-categories>
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="divider"></div>
|
||||
|
||||
<ion-grid
|
||||
*ngIf="marketplace.pkgs | filterPackages: query:category:localPkgs as filtered"
|
||||
*ngIf="store.packages | filterPackages: query:category:localPkgs as filtered"
|
||||
>
|
||||
<div
|
||||
*ngIf="!filtered.length && category === 'updates'"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@@ -12,30 +12,22 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListPage {
|
||||
private readonly pkgs$ = this.marketplaceService.getPackages$()
|
||||
readonly store$ = this.marketplaceService.getSelectedStore$().pipe(
|
||||
filter(Boolean),
|
||||
map(({ info, packages }) => {
|
||||
const categories = new Set<string>()
|
||||
if (info.categories.includes('featured')) categories.add('featured')
|
||||
categories.add('updates')
|
||||
info.categories.forEach(c => categories.add(c))
|
||||
categories.add('all')
|
||||
|
||||
private readonly categories$ = this.marketplaceService
|
||||
.getMarketplaceInfo$()
|
||||
.pipe(
|
||||
map(({ categories }) => {
|
||||
const set = new Set<string>()
|
||||
if (categories.includes('featured')) set.add('featured')
|
||||
set.add('updates')
|
||||
categories.forEach(c => set.add(c))
|
||||
set.add('all')
|
||||
return set
|
||||
}),
|
||||
)
|
||||
|
||||
readonly marketplace$ = combineLatest([this.pkgs$, this.categories$]).pipe(
|
||||
map(arr => {
|
||||
return { pkgs: arr[0], categories: arr[1] }
|
||||
return { categories, packages }
|
||||
}),
|
||||
)
|
||||
|
||||
readonly localPkgs$ = this.patch.watch$('package-data')
|
||||
|
||||
readonly details$ = this.marketplaceService.getUiMarketplace$().pipe(
|
||||
readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
|
||||
map(({ url, name }) => {
|
||||
let color: string
|
||||
let description: string
|
||||
|
||||
@@ -62,7 +62,7 @@ export class MarketplaceShowControlsComponent {
|
||||
|
||||
async tryInstall() {
|
||||
const currentMarketplace = await firstValueFrom(
|
||||
this.marketplaceService.getUiMarketplace$(),
|
||||
this.marketplaceService.getSelectedHost$(),
|
||||
)
|
||||
const url = this.url || currentMarketplace.url
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class MarketplaceShowPage {
|
||||
|
||||
readonly pkg$ = this.loadVersion$.pipe(
|
||||
switchMap(version =>
|
||||
this.marketplaceService.getPackage(this.pkgId, version, this.url),
|
||||
this.marketplaceService.getPackage$(this.pkgId, version, this.url),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export class MarketplacesPage {
|
||||
loader.message = 'Validating marketplace...'
|
||||
await loader.present()
|
||||
|
||||
const name = await this.marketplaceService.validateMarketplace(url)
|
||||
const name = await firstValueFrom(this.marketplaceService.fetchInfo$(url))
|
||||
|
||||
// Save
|
||||
loader.message = 'Saving...'
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { PreferencesPage } from './preferences.page'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PreferencesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
],
|
||||
declarations: [PreferencesPage],
|
||||
})
|
||||
export class PreferencesPageModule {}
|
||||
@@ -1,30 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="embassy"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title (click)="addClick()">Preferences</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group *ngIf="name$ | async as name">
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<ion-item button (click)="presentModalName(name)">
|
||||
<ion-label>Device Name</ion-label>
|
||||
<ion-note slot="end">{{ name.current }}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Marketplace</ion-item-divider>
|
||||
<ion-item
|
||||
*ngIf="ui$ | async as ui"
|
||||
button
|
||||
(click)="serverConfig.presentAlert('auto-check-updates', ui['auto-check-updates'])"
|
||||
>
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-note slot="end">
|
||||
{{ ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,99 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
LoadingController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import {
|
||||
GenericInputComponent,
|
||||
GenericInputOptions,
|
||||
} from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
import { ClientStorageService } from '../../../services/client-storage.service'
|
||||
import {
|
||||
ServerNameInfo,
|
||||
ServerNameService,
|
||||
} from 'src/app/services/server-name.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'preferences',
|
||||
templateUrl: './preferences.page.html',
|
||||
styleUrls: ['./preferences.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PreferencesPage {
|
||||
clicks = 0
|
||||
|
||||
readonly ui$ = this.patch.watch$('ui')
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly name$ = this.serverNameService.name$
|
||||
|
||||
constructor(
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly api: ApiService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
private readonly ClientStorageService: ClientStorageService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly serverNameService: ServerNameService,
|
||||
readonly serverConfig: ServerConfigService,
|
||||
) {}
|
||||
|
||||
async presentModalName(name: ServerNameInfo): Promise<void> {
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: name.default,
|
||||
nullable: true,
|
||||
initialValue: name.current,
|
||||
buttonText: 'Save',
|
||||
submitFn: (value: string) =>
|
||||
this.setDbValue('name', value || name.default),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: GenericInputComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async setDbValue(key: string, value: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.setDbValue([key], value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async addClick() {
|
||||
this.clicks++
|
||||
if (this.clicks >= 5) {
|
||||
this.clicks = 0
|
||||
const newVal = this.ClientStorageService.toggleShowDevTools()
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden',
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
|
||||
await toast.present()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.clicks = Math.max(this.clicks - 1, 0)
|
||||
}, 10000)
|
||||
}
|
||||
}
|
||||
@@ -48,13 +48,6 @@ const routes: Routes = [
|
||||
m => m.ServerMetricsPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'preferences',
|
||||
loadChildren: () =>
|
||||
import('./preferences/preferences.module').then(
|
||||
m => m.PreferencesPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'restore',
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -24,10 +24,7 @@
|
||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider>
|
||||
<ion-text color="dark" *ngIf="cat.key !== 'Power'">
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
<ion-text color="dark" *ngIf="cat.key === 'Power'" (click)="addClick()">
|
||||
<ion-text color="dark" (click)="addClick(cat.key)">
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
</ion-item-divider>
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
LoadingController,
|
||||
NavController,
|
||||
ModalController,
|
||||
ToastController,
|
||||
} from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
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 { firstValueFrom, Observable, of } from 'rxjs'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
@@ -17,6 +18,10 @@ import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page'
|
||||
import { getAllPackages } from '../../../util/get-package-data'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
GenericInputComponent,
|
||||
GenericInputOptions,
|
||||
} from 'src/app/modals/generic-input/generic-input.component'
|
||||
|
||||
@Component({
|
||||
selector: 'server-show',
|
||||
@@ -24,7 +29,8 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
styleUrls: ['server-show.page.scss'],
|
||||
})
|
||||
export class ServerShowPage {
|
||||
clicks = 0
|
||||
settingsClicks = 0
|
||||
powerClicks = 0
|
||||
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly name$ = this.serverNameService.name$
|
||||
@@ -44,8 +50,35 @@ export class ServerShowPage {
|
||||
private readonly ClientStorageService: ClientStorageService,
|
||||
private readonly serverNameService: ServerNameService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly toastCtrl: ToastController,
|
||||
) {}
|
||||
|
||||
async presentModalName(): Promise<void> {
|
||||
const name = await firstValueFrom(this.name$)
|
||||
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: name.default,
|
||||
nullable: true,
|
||||
initialValue: name.current,
|
||||
buttonText: 'Save',
|
||||
submitFn: (value: string) =>
|
||||
this.setDbValue('name', value || name.default),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: GenericInputComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async updateEos(): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: OSUpdatePage,
|
||||
@@ -170,6 +203,32 @@ export class ServerShowPage {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
addClick(title: string) {
|
||||
switch (title) {
|
||||
case 'Settings':
|
||||
this.addSettingsClick()
|
||||
break
|
||||
case 'Power':
|
||||
this.addPowerClick()
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private async setDbValue(key: string, value: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.setDbValue([key], value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// should wipe cache independent of actual BE logout
|
||||
private logout() {
|
||||
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
|
||||
@@ -311,7 +370,7 @@ export class ServerShowPage {
|
||||
{
|
||||
title: 'Software Update',
|
||||
description: 'Get the latest version of embassyOS',
|
||||
icon: 'cog-outline',
|
||||
icon: 'cloud-download-outline',
|
||||
action: () =>
|
||||
this.eosService.updateAvailable$.getValue()
|
||||
? this.updateEos()
|
||||
@@ -320,14 +379,11 @@ export class ServerShowPage {
|
||||
disabled$: this.eosService.updatingOrBackingUp$,
|
||||
},
|
||||
{
|
||||
title: 'Preferences',
|
||||
description: 'Device name, background tasks',
|
||||
icon: 'options-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['preferences'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
title: 'Device Name',
|
||||
description: 'Edit the local display name of your Embassy',
|
||||
icon: 'pricetag-outline',
|
||||
action: () => this.presentModalName(),
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
@@ -504,19 +560,31 @@ export class ServerShowPage {
|
||||
],
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
private async addSettingsClick() {
|
||||
this.settingsClicks++
|
||||
if (this.settingsClicks === 5) {
|
||||
this.settingsClicks = 0
|
||||
const newVal = this.ClientStorageService.toggleShowDevTools()
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden',
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
})
|
||||
|
||||
await toast.present()
|
||||
}
|
||||
}
|
||||
|
||||
addClick() {
|
||||
this.clicks++
|
||||
if (this.clicks >= 5) {
|
||||
this.clicks = 0
|
||||
private addPowerClick() {
|
||||
this.powerClicks++
|
||||
if (this.powerClicks === 5) {
|
||||
this.powerClicks = 0
|
||||
this.ClientStorageService.toggleShowDiskRepair()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.clicks = Math.max(this.clicks - 1, 0)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
frontend/projects/ui/src/app/pages/updates/updates.module.ts
Normal file
33
frontend/projects/ui/src/app/pages/updates/updates.module.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { FilterUpdatesPipe, UpdatesPage } from './updates.page'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { MarkdownPipeModule, SharedPipesModule } from '@start9labs/shared'
|
||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
||||
import { RoundProgressModule } from 'angular-svg-round-progressbar'
|
||||
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: UpdatesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
BadgeMenuComponentModule,
|
||||
SharedPipesModule,
|
||||
SkeletonListComponentModule,
|
||||
MarkdownPipeModule,
|
||||
RoundProgressModule,
|
||||
InstallProgressPipeModule,
|
||||
],
|
||||
declarations: [UpdatesPage, FilterUpdatesPipe],
|
||||
})
|
||||
export class UpdatesPageModule {}
|
||||
84
frontend/projects/ui/src/app/pages/updates/updates.page.html
Normal file
84
frontend/projects/ui/src/app/pages/updates/updates.page.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Updates</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item-group *ngIf="data$ | async as data">
|
||||
<ng-container *ngFor="let host of data.hosts | keyvalue">
|
||||
<ion-item-divider> {{ host.value }} </ion-item-divider>
|
||||
|
||||
<div class="ion-padding-start ion-padding-bottom">
|
||||
<ion-item *ngIf="data.errors.includes(host.key)">
|
||||
<ion-text color="danger">Request Failed</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<ng-container
|
||||
*ngIf="data.marketplace[host.key]?.packages as packages else loading"
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="packages | filterUpdates : data.localPkgs : host.key as updates"
|
||||
>
|
||||
<ion-item *ngFor="let pkg of updates">
|
||||
<ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local">
|
||||
<ion-avatar slot="start">
|
||||
<img [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h1>{{ pkg.manifest.title }}</h1>
|
||||
<h2 class="inline">
|
||||
<span>{{ local.manifest.version }}</span>
|
||||
<ion-icon name="arrow-forward"></ion-icon>
|
||||
<ion-text color="success">
|
||||
{{ pkg.manifest.version }}
|
||||
</ion-text>
|
||||
</h2>
|
||||
<p [innerHTML]="pkg.manifest['release-notes'] | markdown"></p>
|
||||
</ion-label>
|
||||
|
||||
<div slot="end">
|
||||
<round-progress
|
||||
*ngIf="local.state === PackageState.Installing else notInstalling"
|
||||
[current]="local['install-progress'] | installProgress"
|
||||
[max]="100"
|
||||
[radius]="24"
|
||||
[stroke]="4"
|
||||
[rounded]="true"
|
||||
color="var(--ion-color-primary)"
|
||||
></round-progress>
|
||||
<ng-template #notInstalling>
|
||||
<ion-spinner
|
||||
*ngIf="queued[pkg.manifest.id] else updateBtn"
|
||||
color="dark"
|
||||
></ion-spinner>
|
||||
<ng-template #updateBtn>
|
||||
<ion-button
|
||||
(click)="update(pkg.manifest.id, host.key)"
|
||||
color="dark"
|
||||
strong
|
||||
>
|
||||
Update
|
||||
</ion-button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="!updates.length">
|
||||
<p>All services are up to date!</p>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loading>
|
||||
<skeleton-list [showAvatar]="true" [rows]="2"></skeleton-list>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
12
frontend/projects/ui/src/app/pages/updates/updates.page.scss
Normal file
12
frontend/projects/ui/src/app/pages/updates/updates.page.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
ion-avatar {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
ion-label {
|
||||
margin-left: 64px;
|
||||
}
|
||||
|
||||
.name:only-child {
|
||||
display: none;
|
||||
}
|
||||
95
frontend/projects/ui/src/app/pages/updates/updates.page.ts
Normal file
95
frontend/projects/ui/src/app/pages/updates/updates.page.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import {
|
||||
AbstractMarketplaceService,
|
||||
Marketplace,
|
||||
MarketplaceManifest,
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
|
||||
|
||||
interface UpdatesData {
|
||||
hosts: Record<string, string>
|
||||
marketplace: Marketplace
|
||||
localPkgs: Record<string, PackageDataEntry>
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'updates',
|
||||
templateUrl: 'updates.page.html',
|
||||
styleUrls: ['updates.page.scss'],
|
||||
})
|
||||
export class UpdatesPage {
|
||||
queued: Record<string, boolean> = {}
|
||||
|
||||
readonly data$: Observable<UpdatesData> = combineLatest({
|
||||
hosts: this.marketplaceService.getKnownHosts$(),
|
||||
marketplace: this.marketplaceService.getMarketplace$(),
|
||||
localPkgs: this.patch.watch$('package-data'),
|
||||
errors: this.marketplaceService.getRequestErrors$(),
|
||||
})
|
||||
|
||||
readonly PackageState = PackageState
|
||||
readonly rendering = PrimaryRendering[PackageState.Installing]
|
||||
|
||||
constructor(
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
async update(id: string, url: string): Promise<void> {
|
||||
this.queued[id] = true
|
||||
this.api.installPackage({ id, 'marketplace-url': url })
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'filterUpdates',
|
||||
})
|
||||
export class FilterUpdatesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(
|
||||
pkgs: MarketplacePkg[],
|
||||
local: Record<string, PackageDataEntry> = {},
|
||||
url: string,
|
||||
): MarketplacePkg[] {
|
||||
return pkgs.filter(
|
||||
({ manifest }) =>
|
||||
marketplaceSame(manifest, local, url) &&
|
||||
versionLower(manifest, local, this.emver),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function marketplaceSame(
|
||||
{ id }: MarketplaceManifest,
|
||||
local: Record<string, PackageDataEntry>,
|
||||
url: string,
|
||||
): boolean {
|
||||
return local[id]?.installed?.['marketplace-url'] === url
|
||||
}
|
||||
|
||||
export function versionLower(
|
||||
{ version, id }: MarketplaceManifest,
|
||||
local: Record<string, PackageDataEntry>,
|
||||
emver: Emver,
|
||||
): boolean {
|
||||
return (
|
||||
local[id].state === PackageState.Installing ||
|
||||
emver.compare(version, local[id].installed?.manifest.version || '') === 1
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user