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:
Matt Hill
2022-10-27 15:48:12 -06:00
committed by Aiden McClelland
parent d380cc31fa
commit 26c37ba824
53 changed files with 723 additions and 724 deletions

View 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 {}

View 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>
&nbsp;<ion-icon name="arrow-forward"></ion-icon>&nbsp;
<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>

View File

@@ -0,0 +1,12 @@
ion-avatar {
position: absolute;
top: 6px;
}
ion-label {
margin-left: 64px;
}
.name:only-child {
display: none;
}

View 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
)
}