mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
feat: refactor updates (#2860)
This commit is contained in:
20
web/package-lock.json
generated
20
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(/\/$/, '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user