feat: refactor updates (#2860)

This commit is contained in:
Alex Inkin
2025-03-29 22:50:23 +04:00
committed by GitHub
parent 4b4cf76641
commit 1883c9666e
12 changed files with 703 additions and 372 deletions

20
web/package-lock.json generated
View File

@@ -32,6 +32,7 @@
"@taiga-ui/cdk": "4.30.0",
"@taiga-ui/core": "4.30.0",
"@taiga-ui/event-plugins": "4.5.0",
"@taiga-ui/experimental": "4.30.0",
"@taiga-ui/icons": "4.30.0",
"@taiga-ui/kit": "4.30.0",
"@taiga-ui/layout": "4.30.0",
@@ -4567,6 +4568,25 @@
"rxjs": ">=7.0.0"
}
},
"node_modules/@taiga-ui/experimental": {
"version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.30.0.tgz",
"integrity": "sha512-0GWkBinW+tqQIFkWQbTqMBTkKGZhju3RslKRCYbjal/hfcIuSAsEPZqLqIQqVqJNz6AhaIpT0UQ+I7QXzx1/yw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": ">=2.8.1"
},
"peerDependencies": {
"@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0",
"@taiga-ui/addon-commerce": "^4.30.0",
"@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.30.0",
"@taiga-ui/kit": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0"
}
},
"node_modules/@taiga-ui/i18n": {
"version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.30.0.tgz",

View File

@@ -54,6 +54,7 @@
"@taiga-ui/cdk": "4.30.0",
"@taiga-ui/core": "4.30.0",
"@taiga-ui/event-plugins": "4.5.0",
"@taiga-ui/experimental": "4.30.0",
"@taiga-ui/icons": "4.30.0",
"@taiga-ui/kit": "4.30.0",
"@taiga-ui/layout": "4.30.0",

View File

@@ -57,12 +57,12 @@ const ROUTES: Routes = [
loadComponent: () => import('./routes/sideload/sideload.component'),
data: toNavigationItem('/portal/sideload'),
},
// {
// title: systemTabResolver,
// path: 'updates',
// loadComponent: () => import('./routes/updates/updates.component'),
// data: toNavigationItem('/portal/updates'),
// },
{
title: systemTabResolver,
path: 'updates',
loadComponent: () => import('./routes/updates/updates.component'),
data: toNavigationItem('/portal/updates'),
},
{
title: systemTabResolver,
path: 'metrics',

View File

@@ -22,7 +22,7 @@ export class InstallingProgressDisplayPipe implements PipeTransform {
name: 'installingProgress',
})
export class InstallingProgressPipe implements PipeTransform {
transform(progress: T.Progress): number {
transform(progress: T.Progress = false): number {
if (progress === true) return 100
if (progress === false || progress === null || !progress.total) return 0
return Math.floor((100 * progress.done) / progress.total)

View File

@@ -1,40 +1,36 @@
// import { inject, Pipe, PipeTransform } from '@angular/core'
// import { Exver } from '@start9labs/shared'
// import { MarketplacePkg } from '@start9labs/marketplace'
// import {
// InstalledState,
// PackageDataEntry,
// UpdatingState,
// } from 'src/app/services/patch-db/data-model'
import { inject, Pipe, PipeTransform } from '@angular/core'
import { Exver } from '@start9labs/shared'
import { MarketplacePkg } from '@start9labs/marketplace'
import {
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
// @Pipe({
// name: 'filterUpdates',
// standalone: true,
// })
// export class FilterUpdatesPipe implements PipeTransform {
// private readonly exver = inject(Exver)
@Pipe({
name: 'filterUpdates',
standalone: true,
})
export class FilterUpdatesPipe implements PipeTransform {
private readonly exver = inject(Exver)
// transform(
// pkgs?: MarketplacePkg[],
// local: Record<
// string,
// PackageDataEntry<InstalledState | UpdatingState>
// > = {},
// ): MarketplacePkg[] | null {
// return (
// pkgs?.filter(
// ({ id, version, flavor }) =>
// local[id] &&
// this.exver.getFlavor(getVersion(local, id)) === flavor &&
// this.exver.compareExver(version, getVersion(local, id)) === 1,
// ) || null
// )
// }
// }
// function getVersion(
// local: Record<string, PackageDataEntry<InstalledState | UpdatingState>>,
// id: string,
// ): string {
// return local[id].stateInfo.manifest.version
// }
transform(
pkgs: MarketplacePkg[],
local: Record<
string,
PackageDataEntry<InstalledState | UpdatingState>
> = {},
): MarketplacePkg[] {
return pkgs.filter(({ id, version, flavor }) => {
const localPkg = local[id]
return (
localPkg &&
this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor &&
this.exver.compareExver(
version,
localPkg.stateInfo.manifest.version,
) === 1
)
})
}
}

View File

@@ -1,199 +1,311 @@
// import { Component, inject, Input } from '@angular/core'
// import { RouterLink } from '@angular/router'
// import {
// MarketplacePkg,
// } from '@start9labs/marketplace'
// import {
// MarkdownPipeModule,
// SafeLinksDirective,
// SharedPipesModule,
// } from '@start9labs/shared'
// import {
// TuiDialogService,
// TuiLoader,
// TuiIcon,
// TuiLink,
// TuiButton,
// } from '@taiga-ui/core'
// import {
// TuiProgress,
// TuiAccordion,
// TuiAvatar,
// TUI_CONFIRM,
// } from '@taiga-ui/kit'
// import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
// import { PatchDB } from 'patch-db-client'
// import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
// import { MarketplaceService } from 'src/app/services/marketplace.service'
// import {
// DataModel,
// InstalledState,
// PackageDataEntry,
// UpdatingState,
// } from 'src/app/services/patch-db/data-model'
// import { getAllPackages } from 'src/app/utils/get-package-data'
// import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { DatePipe } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
inject,
input,
signal,
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { MarketplacePkg } from '@start9labs/marketplace'
import { MarkdownPipeModule, SafeLinksDirective } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import {
TuiButton,
TuiIcon,
TuiLink,
TuiLoader,
TuiTitle,
} from '@taiga-ui/core'
import { TuiExpand } from '@taiga-ui/experimental'
import {
TUI_CONFIRM,
TuiAvatar,
TuiChevron,
TuiFade,
TuiProgressCircle,
} from '@taiga-ui/kit'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import {
DataModel,
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { getAllPackages } from 'src/app/utils/get-package-data'
import { hasCurrentDeps } from 'src/app/utils/has-deps'
import UpdatesComponent from './updates.component'
// @Component({
// selector: 'updates-item',
// template: `
// <tui-accordion-item borders="top-bottom">
// <div class="g-action">
// <tui-avatar size="s">
// <img alt="" [src]="marketplacePkg.icon" />
// </tui-avatar>
// <div [style.flex]="1" [style.overflow]="'hidden'">
// <strong>{{ marketplacePkg.title }}</strong>
// <div>
// {{ localPkg.stateInfo.manifest.version }}
// <tui-icon icon="@tui.arrow-right" [style.font-size.rem]="1" />
// <span [style.color]="'var(--tui-text-positive)'">
// {{ marketplacePkg.version }}
// </span>
// </div>
// <div [style.color]="'var(--tui-text-negative)'">{{ errors }}</div>
// </div>
// @if (localPkg.stateInfo.state === 'updating') {
// <tui-progress-circle
// class="g-positive"
// size="s"
// [max]="1"
// [value]="
// (localPkg.stateInfo.installingInfo.progress.overall
// | installingProgress) || 0
// "
// />
// } @else {
// @if (ready) {
// <button
// tuiButton
// size="s"
// [appearance]="errors ? 'destructive' : 'primary'"
// (click.stop)="onClick()"
// >
// {{ errors ? 'Retry' : 'Update' }}
// </button>
// } @else {
// <tui-loader [style.width.rem]="2" [inheritColor]="true" />
// }
// }
// </div>
// <ng-template tuiAccordionItemContent>
// <strong>What's new</strong>
// <p
// safeLinks
// [innerHTML]="marketplacePkg.releaseNotes | markdown | dompurify"
// ></p>
// <a
// tuiLink
// iconEnd="@tui.external-link"
// routerLink="/marketplace"
// [queryParams]="{ url: url, id: marketplacePkg.id }"
// >
// View listing
// </a>
// </ng-template>
// </tui-accordion-item>
// `,
// styles: [
// `
// :host {
// display: block;
// --tui-background-neutral-1-hover: transparent;
@Component({
standalone: true,
selector: 'updates-item',
template: `
<tr (click)="expanded.set(!expanded())">
<td>
<div [style.gap.rem]="0.75">
<tui-avatar size="s"><img alt="" [src]="item().icon" /></tui-avatar>
<span tuiTitle [style.margin]="'-0.125rem 0 0'">
<b tuiFade>{{ item().title }}</b>
<span tuiSubtitle tuiFade class="mobile">
<span class="g-secondary">
{{ local().stateInfo.manifest.version }}
</span>
<tui-icon icon="@tui.arrow-right" />
<span class="g-positive">{{ item().version }}</span>
</span>
</span>
</div>
</td>
<td class="desktop">
<div>
<span class="g-secondary">
{{ local().stateInfo.manifest.version }}
</span>
<tui-icon icon="@tui.arrow-right" />
<span class="g-positive">{{ item().version }}</span>
</div>
</td>
<td class="desktop">{{ item().gitHash }}</td>
<td class="desktop">{{ item().s9pk.publishedAt | date }}</td>
<td>
<div>
@if (local().stateInfo.state === 'updating') {
<tui-progress-circle
class="g-positive"
size="xs"
[max]="100"
[value]="
(local().stateInfo.installingInfo?.progress?.overall
| installingProgress) || 0
"
/>
} @else {
@if (ready()) {
<button
tuiButton
iconStart="@tui.arrow-big-up-dash"
[appearance]="error() ? 'destructive' : 'primary'"
(click.stop)="onClick()"
>
{{ error() ? 'Retry' : 'Update' }}
</button>
} @else {
<tui-loader [style.width.rem]="2" [inheritColor]="true" />
}
}
<button tuiIconButton appearance="icon" [tuiChevron]="expanded()">
Show more
</button>
</div>
</td>
</tr>
<tr>
<td colspan="5">
@if (error()) {
<p class="g-negative">{{ error() }}</p>
}
<tui-expand [expanded]="expanded()">
<p tuiTitle class="mobile">
<b>Package Hash</b>
<span tuiSubtitle>{{ item().gitHash }}</span>
</p>
<p tuiTitle class="mobile">
<b>Published</b>
<span tuiSubtitle>{{ item().s9pk.publishedAt | date }}</span>
</p>
<p tuiTitle>
<span>
<a
tuiLink
iconEnd="@tui.external-link"
routerLink="/portal/marketplace"
[queryParams]="{ url: parent.current()?.url, id: item().id }"
>
View listing
</a>
<b>What's new</b>
</span>
</p>
<p
safeLinks
[innerHTML]="item().releaseNotes | markdown | dompurify"
></p>
</tui-expand>
</td>
</tr>
`,
styles: `
@import '@taiga-ui/core/styles/taiga-ui-local';
// &:not(:last-child) {
// border-bottom: 1px solid var(--tui-background-neutral-1);
// }
// }
// `,
// ],
// standalone: true,
// imports: [
// RouterLink,
// MarkdownPipeModule,
// NgDompurifyModule,
// SafeLinksDirective,
// SharedPipesModule,
// TuiProgress,
// TuiAccordion,
// TuiAvatar,
// TuiIcon,
// TuiButton,
// TuiLink,
// TuiLoader,
// InstallingProgressPipe,
// ],
// })
// export class UpdatesItemComponent {
// private readonly dialogs = inject(TuiDialogService)
// private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
// private readonly marketplaceService = inject(MarketplaceService)
:host {
display: contents;
}
// @Input({ required: true })
// marketplacePkg!: MarketplacePkg
tui-icon {
font-size: 1rem;
}
// @Input({ required: true })
// localPkg!: PackageDataEntry<InstalledState | UpdatingState>
div {
display: flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
}
// @Input({ required: true })
// url!: string
tr:first-child {
min-height: var(--tui-height-l);
word-break: break-word;
clip-path: inset(0 round var(--tui-radius-s));
cursor: pointer;
@include transition(background);
// get pkgId(): string {
// return this.marketplacePkg.id
// }
@media ($tui-mouse) {
&:hover {
background: var(--tui-background-neutral-1);
}
}
}
// get errors(): string {
// return this.marketplaceService.updateErrors[this.pkgId]
// }
td {
min-width: 0;
vertical-align: middle;
// get ready(): boolean {
// return !this.marketplaceService.updateQueue[this.pkgId]
// }
&:first-child {
white-space: nowrap;
}
// async onClick() {
// const { id } = this.marketplacePkg
&:last-child {
text-align: right;
white-space: nowrap;
// delete this.marketplaceService.updateErrors[id]
// this.marketplaceService.updateQueue[id] = true
div {
justify-content: flex-end;
}
}
// if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
// const proceed = await this.alert()
&[colspan]:only-child {
padding: 0 3rem;
text-align: left;
// if (proceed) {
// await this.update()
// } else {
// delete this.marketplaceService.updateQueue[id]
// }
// } else {
// await this.update()
// }
// }
[tuiLink] {
float: right;
}
}
}
// private async update() {
// const { id, version } = this.marketplacePkg
[tuiTitle] {
margin: 1rem 0;
}
// try {
// await this.marketplaceService.installPackage(id, version, this.url)
// delete this.marketplaceService.updateQueue[id]
// } catch (e: any) {
// delete this.marketplaceService.updateQueue[id]
// this.marketplaceService.updateErrors[id] = e.message
// }
// }
.mobile {
display: none;
}
// private async alert(): Promise<boolean> {
// return new Promise(async resolve => {
// this.dialogs
// .open<boolean>(TUI_CONFIRM, {
// label: 'Warning',
// size: 's',
// data: {
// content: `Services that depend on ${this.localPkg.stateInfo.manifest.title} will no longer work properly and may crash`,
// yes: 'Continue',
// no: 'Cancel',
// },
// })
// .subscribe(response => resolve(response))
// })
// }
// }
:host-context(tui-root._mobile) {
tr:first-child {
display: grid;
grid-template-columns: 1fr min-content;
align-items: center;
padding: 0.5rem 0;
}
td[colspan]:only-child {
padding: 0 0.5rem;
}
[tuiButton] {
font-size: 0;
gap: 0;
}
.desktop {
display: none;
}
.mobile {
display: flex;
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
RouterLink,
TuiExpand,
TuiButton,
TuiChevron,
TuiAvatar,
TuiLink,
TuiIcon,
TuiLoader,
TuiProgressCircle,
TuiTitle,
MarkdownPipeModule,
NgDompurifyModule,
SafeLinksDirective,
DatePipe,
InstallingProgressPipe,
TuiFade,
],
})
export class UpdatesItemComponent {
private readonly dialogs = inject(TuiResponsiveDialogService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly service = inject(MarketplaceService)
readonly parent = inject(UpdatesComponent)
readonly expanded = signal(false)
readonly error = signal('')
readonly ready = signal(true)
readonly item = input.required<MarketplacePkg>()
readonly local =
input.required<PackageDataEntry<InstalledState | UpdatingState>>()
async onClick() {
this.ready.set(false)
this.error.set('')
if (hasCurrentDeps(this.item().id, await getAllPackages(this.patch))) {
if (await this.alert()) {
await this.update()
} else {
this.ready.set(true)
}
} else {
await this.update()
}
}
private async update() {
const { id, version } = this.item()
const url = this.parent.current()?.url || ''
try {
await this.service.installPackage(id, version, url)
this.ready.set(true)
} catch (e: any) {
this.ready.set(true)
this.error.set(e.message)
}
}
private async alert(): Promise<boolean> {
return firstValueFrom(
this.dialogs
.open<boolean>(TUI_CONFIRM, {
label: 'Warning',
size: 's',
data: {
content: `Services that depend on ${this.local().stateInfo.manifest.title} will no longer work properly and may crash`,
yes: 'Continue',
no: 'Cancel',
},
})
.pipe(defaultIfEmpty(false)),
)
}
}

View File

@@ -1,95 +1,251 @@
// import { CommonModule } from '@angular/common'
// import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
// import {
// StoreIconComponentModule,
// } from '@start9labs/marketplace'
// import { TuiAvatar } from '@taiga-ui/kit'
// import { TuiCell } from '@taiga-ui/layout'
// import { PatchDB } from 'patch-db-client'
// import { combineLatest, map } from 'rxjs'
// import { FilterUpdatesPipe } from 'src/app/routes/portal/routes/updates/filter-updates.pipe'
// import { UpdatesItemComponent } from 'src/app/routes/portal/routes/updates/item.component'
// import { ConfigService } from 'src/app/services/config.service'
// import { MarketplaceService } from 'src/app/services/marketplace.service'
// import {
// DataModel,
// InstalledState,
// PackageDataEntry,
// UpdatingState,
// } from 'src/app/services/patch-db/data-model'
// import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
Marketplace,
StoreIconComponentModule,
StoreIdentity,
} from '@start9labs/marketplace'
import { TuiTable } from '@taiga-ui/addon-table'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
import { TuiButton, TuiNotification, TuiTitle } from '@taiga-ui/core'
import {
TuiAvatar,
TuiBadgeNotification,
TuiFade,
TuiSkeleton,
} from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, tap } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import {
DataModel,
InstalledState,
PackageDataEntry,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service'
import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
import { FilterUpdatesPipe } from './filter-updates.pipe'
import { UpdatesItemComponent } from './item.component'
// @Component({
// template: `
// @if (data$ | async; as data) {
// @for (host of data.hosts; track host) {
// <h3 class="g-title">
// <store-icon [url]="host.url" [marketplace]="mp" size="26px" />
// {{ host.name }}
// </h3>
// @if (data.errors.includes(host.url)) {
// <p class="g-negative">Request Failed</p>
// }
// @if (data.mp[host.url]?.packages | filterUpdates: data.local; as pkgs) {
// @for (pkg of pkgs; track pkg) {
// <updates-item
// [marketplacePkg]="pkg"
// [localPkg]="data.local[pkg.id]"
// [url]="host.url"
// />
// } @empty {
// <p>All services are up to date!</p>
// }
// } @else {
// @for (i of [0, 1, 2]; track i) {
// <section tuiCell>
// <tui-avatar class="tui-skeleton" />
// <span class="tui-skeleton">Loading update item</span>
// <span class="tui-skeleton" [style.margin-left]="'auto'">
// Loading actions
// </span>
// </section>
// }
// }
// }
// }
// `,
// host: { class: 'g-page' },
// changeDetection: ChangeDetectionStrategy.OnPush,
// standalone: true,
// imports: [
// CommonModule,
// TuiCell,
// TuiAvatar,
// StoreIconComponentModule,
// FilterUpdatesPipe,
// UpdatesItemComponent,
// ],
// })
// export default class UpdatesComponent {
// private readonly marketplaceService = inject(MarketplaceService)
interface UpdatesData {
hosts: StoreIdentity[]
marketplace: Marketplace
localPkgs: Record<string, PackageDataEntry<InstalledState | UpdatingState>>
errors: string[]
}
// readonly mp = inject(ConfigService).marketplace
// readonly data$ = combineLatest({
// hosts: this.marketplaceService.getKnownHosts$(true),
// mp: this.marketplaceService.getMarketplace$(),
// local: inject<PatchDB<DataModel>>(PatchDB)
// .watch$('packageData')
// .pipe(
// map(pkgs =>
// Object.entries(pkgs).reduce(
// (acc, [id, val]) => {
// if (isInstalled(val) || isUpdating(val))
// return { ...acc, [id]: val }
// return acc
// },
// {} as Record<
// string,
// PackageDataEntry<InstalledState | UpdatingState>
// >,
// ),
// ),
// ),
// errors: this.marketplaceService.getRequestErrors$(),
// })
// }
@Component({
template: `
<ng-container *title>
<label>Updates</label>
@if (current()) {
<button
tuiIconButton
iconStart="@tui.arrow-left"
(click)="current.set(null)"
>
Back
</button>
{{ current()?.name }}
}
</ng-container>
<aside class="g-aside">
@for (registry of data()?.hosts; track $index) {
<button
tuiCell
[class.g-secondary]="current()?.url !== registry.url"
(click)="current.set(registry)"
>
<tui-avatar>
<store-icon [url]="registry.url" [marketplace]="mp" />
</tui-avatar>
<span tuiTitle>
<b tuiFade>{{ registry.name }}</b>
<span tuiSubtitle tuiFade>{{ clear(registry.url) }}</span>
</span>
@if (
(
data()?.marketplace?.[registry.url]?.packages || []
| filterUpdates: data()?.localPkgs
).length;
as length
) {
<tui-badge-notification>
{{ length }}
</tui-badge-notification>
}
</button>
}
</aside>
<section class="g-subpage">
@if (data()?.errors?.includes(current()?.url || '')) {
<tui-notification appearance="negative">
Request Failed
</tui-notification>
}
<section class="g-card" [style.padding]="'0 1rem 1rem'">
<table tuiTable class="g-table">
<thead>
<tr>
<th tuiTh>Name</th>
<th tuiTh>Version</th>
<th tuiTh>Package Hash</th>
<th tuiTh>Published</th>
<th tuiTh></th>
</tr>
</thead>
<tbody>
@if (
data()?.marketplace?.[current()?.url || '']?.packages;
as packages
) {
@if (packages | filterUpdates: data()?.localPkgs; as updates) {
@for (pkg of updates; track $index) {
<updates-item
[item]="pkg"
[local]="data()?.localPkgs?.[pkg.id]!"
/>
} @empty {
<tr>
<td colspan="5">All services are up to date!</td>
</tr>
}
}
} @else {
<tr>
<td colspan="5" [tuiSkeleton]="true">Loading</td>
</tr>
<tr>
<td colspan="5" [tuiSkeleton]="true">Loading</td>
</tr>
}
</tbody>
</table>
</section>
</section>
`,
styles: `
:host {
display: flex;
padding: 0;
}
aside {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding-top: 1rem;
color: var(--tui-text-secondary);
}
label:not(:last-child) {
display: none;
}
[tuiCell] {
white-space: nowrap;
&:not(.g-secondary) {
background: var(--tui-background-neutral-1);
box-shadow: inset 0 0 0 1px var(--tui-background-neutral-1-hover);
color: var(--tui-text-primary);
}
}
td {
clip-path: inset(0.5rem round var(--tui-radius-s));
}
:host-context(tui-root._mobile) {
aside {
width: 100%;
}
section {
background: none;
box-shadow: none;
}
[tuiCell] {
color: var(--tui-text-primary) !important;
}
:host._selected {
aside {
display: none;
}
}
:host:not(._selected) {
section {
display: none;
}
}
}
`,
host: {
class: 'g-page',
'[class._selected]': 'current()',
},
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
TuiCell,
TuiAvatar,
TuiTitle,
TuiNotification,
TuiSkeleton,
TuiTable,
TuiBadgeNotification,
TuiFade,
TuiButton,
StoreIconComponentModule,
FilterUpdatesPipe,
UpdatesItemComponent,
TitleDirective,
],
})
export default class UpdatesComponent {
private readonly isMobile = inject(TUI_IS_MOBILE)
private readonly marketplaceService = inject(MarketplaceService)
readonly mp = inject(ConfigService).marketplace
readonly current = signal<StoreIdentity | null>(null)
readonly data = toSignal<UpdatesData>(
combineLatest({
hosts: this.marketplaceService
.getKnownHosts$(true)
.pipe(tap(([store]) => !this.isMobile && this.current.set(store))),
marketplace: this.marketplaceService.marketplace$,
localPkgs: inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData')
.pipe(
map(pkgs =>
Object.entries(pkgs).reduce<
Record<string, PackageDataEntry<InstalledState | UpdatingState>>
>(
(acc, [id, val]) =>
isInstalled(val) || isUpdating(val)
? { ...acc, [id]: val }
: acc,
{},
),
),
),
errors: this.marketplaceService.getRequestErrors$(),
}),
)
clear(url: string): string {
return url.replace(/https?:\/\//, '').replace(/\/$/, '')
}
}

View File

@@ -1054,11 +1054,11 @@ export class MockApiService extends ApiService {
...Mock.LocalPkgs[params.id],
stateInfo: {
// if installing
state: 'installing',
// state: 'installing',
// if updating
// state: 'updating',
// manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
state: 'updating',
manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
// both
installingInfo: {

View File

@@ -9,14 +9,15 @@ import {
map,
Observable,
pairwise,
shareReplay,
startWith,
switchMap,
} from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service'
import { EOSService } from 'src/app/services/eos.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { NotificationService } from 'src/app/services/notification.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { ConnectionService } from 'src/app/services/connection.service'
import { getManifest } from 'src/app/utils/get-package-data'
@Injectable({
@@ -54,35 +55,35 @@ export class BadgeService {
),
)
// private readonly updates$ = combineLatest([
// this.marketplaceService.getMarketplace$(true),
// this.local$,
// ]).pipe(
// map(
// ([marketplace, local]) =>
// Object.entries(marketplace).reduce(
// (list, [_, store]) =>
// store?.packages.reduce(
// (result, { id, version }) =>
// local[id] &&
// this.exver.compareExver(
// version,
// getManifest(local[id]).version,
// ) === 1
// ? result.add(id)
// : result,
// list,
// ) || list,
// new Set<string>(),
// ).size,
// ),
// shareReplay(1),
// )
private readonly updates$ = combineLatest([
this.marketplaceService.marketplace$,
this.local$,
]).pipe(
map(
([marketplace, local]) =>
Object.entries(marketplace).reduce(
(list, [_, store]) =>
store?.packages.reduce(
(result, { id, version }) =>
local[id] &&
this.exver.compareExver(
version,
getManifest(local[id]).version,
) === 1
? result.add(id)
: result,
list,
) || list,
new Set<string>(),
).size,
),
shareReplay(1),
)
getCount(id: string): Observable<number> {
switch (id) {
// case '/portal/updates':
// return this.updates$
case '/portal/updates':
return this.updates$
case '/portal/system':
return this.system$
case '/portal/notifications':

View File

@@ -1,32 +1,39 @@
import { Injectable } from '@angular/core'
import {
StoreIdentity,
MarketplacePkg,
GetPackageRes,
Marketplace,
MarketplacePkg,
StoreData,
StoreDataWithUrl,
StoreIdentity,
} from '@start9labs/marketplace'
import { Exver, sameUrl } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PatchDB } from 'patch-db-client'
import {
BehaviorSubject,
catchError,
combineLatest,
distinctUntilChanged,
filter,
from,
map,
mergeMap,
Observable,
of,
shareReplay,
switchMap,
distinctUntilChanged,
pairwise,
ReplaySubject,
scan,
shareReplay,
startWith,
switchMap,
tap,
} from 'rxjs'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { ConfigService } from './config.service'
import { Exver } from '@start9labs/shared'
import { ClientStorageService } from './client-storage.service'
import { T } from '@start9labs/start-sdk'
import { ConfigService } from './config.service'
@Injectable({
providedIn: 'root',
@@ -66,7 +73,10 @@ export class MarketplaceService {
const { start9, community } = this.config.marketplace
let arr = [
toStoreIdentity(start9, hosts[start9]),
toStoreIdentity(community, hosts[community]),
toStoreIdentity(community, {
...hosts[community],
name: 'Community Registry',
}),
]
return arr.concat(
@@ -93,6 +103,32 @@ export class MarketplaceService {
private readonly requestErrors$ = new BehaviorSubject<string[]>([])
readonly marketplace$: Observable<Marketplace> = this.knownHosts$.pipe(
startWith<StoreIdentity[]>([]),
pairwise(),
mergeMap(([prev, curr]) =>
curr.filter(c => !prev.find(p => sameUrl(c.url, p.url))),
),
mergeMap(({ url, name }) =>
this.fetchRegistry$(url).pipe(
tap(data => {
if (data?.info.name) this.updateStoreName(url, name, data.info.name)
}),
map<StoreData | null, [string, StoreData | null]>(data => [url, data]),
startWith<[string, StoreData | null]>([url, null]),
),
),
scan<[string, StoreData | null], Record<string, StoreData | null>>(
(requests, [url, store]) => {
requests[url] = store
return requests
},
{},
),
shareReplay({ bufferSize: 1, refCount: true }),
)
constructor(
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
@@ -230,10 +266,6 @@ export class MarketplaceService {
}
}
// UI only
readonly updateErrors: Record<string, string> = {}
readonly updateQueue: Record<string, boolean> = {}
getRequestErrors$(): Observable<string[]> {
return this.requestErrors$
}
@@ -251,6 +283,19 @@ export class MarketplaceService {
await this.api.installPackage(params)
}
private async updateStoreName(
url: string,
oldName: string | undefined,
newName: string,
): Promise<void> {
if (oldName !== newName) {
this.api.setDbValue<string>(
['marketplace', 'knownHosts', url, 'name'],
newName,
)
}
}
}
function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity {

View File

@@ -16,11 +16,10 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
icon: '@tui.upload',
title: 'Sideload',
},
// @TODO 040
// '/portal/updates': {
// icon: '@tui.globe',
// title: 'Updates',
// },
'/portal/updates': {
icon: '@tui.globe',
title: 'Updates',
},
// @TODO 041
// '/portal/backups': {
// icon: '@tui.save',

View File

@@ -69,6 +69,7 @@ hr {
min-height: fit-content;
flex: 1;
padding: 1rem;
overflow: hidden;
}
.g-aside {
@@ -76,7 +77,7 @@ hr {
top: 1px;
left: 1px;
margin: 1px;
width: 16rem;
width: 18rem;
padding: 0.5rem;
text-transform: capitalize;
box-shadow: 1px 0 var(--tui-border-normal);