mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
rework installing page and add cancel install button (#2915)
* rework installing page and add cancel install button * actually call cancel endpoint * fix two bugs * include translations in progress component * cancellable installs * fix: comments (#2916) * fix: comments * delete comments * ensure trailing slash and no qp for new registry url --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix raspi * bump sdk --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
@@ -55,7 +55,7 @@ export class AppComponent {
|
||||
)
|
||||
.subscribe({
|
||||
complete: async () => {
|
||||
const loader = this.loader.open('' as i18nKey).subscribe()
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
try {
|
||||
await this.api.reboot()
|
||||
|
||||
@@ -17,7 +17,6 @@ import { StoreIconComponentModule } from './store-icon/store-icon.component.modu
|
||||
<ng-content />
|
||||
}
|
||||
`,
|
||||
styles: [':host { border-radius: 0.25rem; width: stretch; }'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [StoreIconComponentModule, TuiIcon, TuiTitle],
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ export type StoreIdentity = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type Marketplace = Record<string, StoreData | null>
|
||||
export type Marketplace = Record<string, StoreDataWithUrl | null>
|
||||
|
||||
export type StoreData = {
|
||||
info: T.RegistryInfo
|
||||
|
||||
@@ -362,8 +362,8 @@ export default {
|
||||
359: 'Die Partition enthält keine gültige Sicherung',
|
||||
360: 'Sicherungsfortschritt',
|
||||
361: 'Abgeschlossen',
|
||||
362: 'Sicherung läuft',
|
||||
363: 'Warten',
|
||||
362: 'sicherung läuft',
|
||||
363: 'warten',
|
||||
364: 'Sicherung erstellt',
|
||||
365: 'Wiederherstellung ausgewählt',
|
||||
366: 'Initialisierung',
|
||||
@@ -493,4 +493,9 @@ export default {
|
||||
490: 'deutsch',
|
||||
491: 'englisch',
|
||||
492: 'Startmenü',
|
||||
493: 'Installationsfortschritt',
|
||||
494: 'Herunterladen',
|
||||
495: 'Validierung',
|
||||
496: 'in Bearbeitung',
|
||||
497: 'abgeschlossen',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -361,8 +361,8 @@ export const ENGLISH = {
|
||||
'Drive partition does not contain a valid backup': 359,
|
||||
'Backup Progress': 360,
|
||||
'Complete': 361,
|
||||
'Backing up': 362,
|
||||
'Waiting': 363,
|
||||
'backing up': 362,
|
||||
'waiting': 363,
|
||||
'Backup made': 364,
|
||||
'Restore selected': 365,
|
||||
'Initializing': 366,
|
||||
@@ -492,4 +492,9 @@ export const ENGLISH = {
|
||||
'german': 490,
|
||||
'english': 491,
|
||||
'Start Menu': 492,
|
||||
'Install Progress': 493,
|
||||
'Downloading': 494,
|
||||
'Validating': 495,
|
||||
'in progress': 496,
|
||||
'complete': 497,
|
||||
} as const
|
||||
|
||||
@@ -362,8 +362,8 @@ export default {
|
||||
359: 'La partición de la unidad no contiene una copia de seguridad válida',
|
||||
360: 'Progreso de la copia de seguridad',
|
||||
361: 'Completo',
|
||||
362: 'Haciendo copia de seguridad',
|
||||
363: 'Esperando',
|
||||
362: 'haciendo copia de seguridad',
|
||||
363: 'esperando',
|
||||
364: 'Copia de seguridad realizada',
|
||||
365: 'Restauración seleccionada',
|
||||
366: 'Inicializando',
|
||||
@@ -493,4 +493,9 @@ export default {
|
||||
490: 'alemán',
|
||||
491: 'inglés',
|
||||
492: 'Menú de Inicio',
|
||||
} as any satisfies i18n
|
||||
493: 'Progreso de instalación',
|
||||
494: 'Descargando',
|
||||
495: 'Validando',
|
||||
496: 'en progreso',
|
||||
497: 'completo',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -362,8 +362,8 @@ export default {
|
||||
359: 'Partycja dysku nie zawiera prawidłowej kopii zapasowej',
|
||||
360: 'Postęp tworzenia kopii zapasowej',
|
||||
361: 'Zakończono',
|
||||
362: 'Tworzenie kopii zapasowej',
|
||||
363: 'Oczekiwanie',
|
||||
362: 'tworzenie kopii zapasowej',
|
||||
363: 'oczekiwanie',
|
||||
364: 'Kopia zapasowa utworzona',
|
||||
365: 'Wybrano przywracanie',
|
||||
366: 'Inicjalizacja',
|
||||
@@ -493,4 +493,9 @@ export default {
|
||||
490: 'niemiecki',
|
||||
491: 'angielski',
|
||||
492: 'Menu Startowe',
|
||||
493: 'Postęp instalacji',
|
||||
494: 'Pobieranie',
|
||||
495: 'Weryfikowanie',
|
||||
496: 'w toku',
|
||||
497: 'zakończono',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -39,7 +39,7 @@ class LoadingComponent {
|
||||
useFactory: () => new LoadingService(TUI_DIALOGS, LoadingComponent),
|
||||
})
|
||||
export class LoadingService extends TuiPopoverService<unknown> {
|
||||
override open<G = void>(textContent: i18nKey) {
|
||||
override open<G = void>(textContent: i18nKey | '' = '') {
|
||||
return super.open<G>(textContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class MarketplaceControlsComponent {
|
||||
async tryInstall() {
|
||||
const currentUrl = this.file
|
||||
? null
|
||||
: await firstValueFrom(this.marketplaceService.getCurrentRegistryUrl$())
|
||||
: await firstValueFrom(this.marketplaceService.currentRegistryUrl$)
|
||||
const originalUrl = this.localPkg?.registry || null
|
||||
|
||||
if (!this.localPkg) {
|
||||
|
||||
@@ -53,8 +53,7 @@ import { DialogService, i18nPipe } from '@start9labs/shared'
|
||||
})
|
||||
export class MarketplaceMenuComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly marketplaceService = inject(MarketplaceService)
|
||||
readonly registry$ = this.marketplaceService.getCurrentRegistry$()
|
||||
readonly registry$ = inject(MarketplaceService).currentRegistry$
|
||||
|
||||
changeRegistry() {
|
||||
this.dialog
|
||||
|
||||
@@ -29,9 +29,7 @@ import { StorageService } from 'src/app/services/storage.service'
|
||||
<div class="marketplace-content-inner">
|
||||
<marketplace-notification [url]="(url$ | async) || ''" />
|
||||
<div class="title-wrapper">
|
||||
<h1>
|
||||
{{ category$ | async | titlecase }}
|
||||
</h1>
|
||||
<h1>{{ category$ | async | titlecase }}</h1>
|
||||
</div>
|
||||
@if (registry$ | async; as registry) {
|
||||
<section class="marketplace-content-list">
|
||||
@@ -178,14 +176,14 @@ export default class MarketplaceComponent {
|
||||
queryParamsHandling: 'merge',
|
||||
})
|
||||
} else {
|
||||
this.marketplaceService.setRegistryUrl(registry)
|
||||
this.marketplaceService.currentRegistryUrl$.next(registry)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
readonly url$ = this.marketplaceService.getCurrentRegistryUrl$()
|
||||
readonly url$ = this.marketplaceService.currentRegistryUrl$
|
||||
readonly category$ = this.categoryService.getCategory$()
|
||||
readonly query$ = this.categoryService.getQuery$()
|
||||
readonly registry$ = this.marketplaceService.getCurrentRegistry$()
|
||||
readonly registry$ = this.marketplaceService.currentRegistry$
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export class MarketplacePreviewComponent {
|
||||
|
||||
readonly flavors$ = this.flavor$.pipe(
|
||||
switchMap(current =>
|
||||
this.marketplaceService.getCurrentRegistry$().pipe(
|
||||
this.marketplaceService.currentRegistry$.pipe(
|
||||
map(({ packages }) =>
|
||||
packages.filter(
|
||||
({ id, flavor }) => id === this.pkgId && flavor !== current,
|
||||
|
||||
@@ -41,7 +41,7 @@ import { StorageService } from 'src/app/services/storage.service'
|
||||
></button>
|
||||
}
|
||||
<h3 class="g-title">{{ 'Custom Registries' | i18n }}</h3>
|
||||
<button tuiCell (click)="add()" [style.width]="'-webkit-fill-available'">
|
||||
<button tuiCell (click)="add()">
|
||||
<tui-icon icon="@tui.plus" [style.margin-inline.rem]="'0.5'" />
|
||||
<div tuiTitle>{{ 'Add custom registry' | i18n }}</div>
|
||||
</button>
|
||||
@@ -71,6 +71,10 @@ import { StorageService } from 'src/app/services/storage.service'
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[tuiCell] {
|
||||
width: stretch;
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -102,8 +106,8 @@ export class MarketplaceRegistryModal {
|
||||
private readonly storage = inject(StorageService)
|
||||
|
||||
readonly registries$ = combineLatest([
|
||||
this.marketplaceService.getRegistries$(),
|
||||
this.marketplaceService.getCurrentRegistryUrl$(),
|
||||
this.marketplaceService.registries$,
|
||||
this.marketplaceService.currentRegistryUrl$,
|
||||
]).pipe(
|
||||
map(([registries, currentUrl]) =>
|
||||
registries.map(s => ({
|
||||
@@ -185,7 +189,7 @@ export class MarketplaceRegistryModal {
|
||||
loader.closed = false
|
||||
loader.add(this.loader.open('Changing registry').subscribe())
|
||||
try {
|
||||
this.marketplaceService.setRegistryUrl(url)
|
||||
this.marketplaceService.currentRegistryUrl$.next(url)
|
||||
this.router.navigate([], {
|
||||
queryParams: { registry: url },
|
||||
queryParamsHandling: 'merge',
|
||||
@@ -231,7 +235,7 @@ export class MarketplaceRegistryModal {
|
||||
|
||||
private async save(rawUrl: string, connect = false): Promise<boolean> {
|
||||
const loader = this.loader.open('Loading').subscribe()
|
||||
const url = new URL(rawUrl).toString()
|
||||
const url = new URL(rawUrl).origin + '/'
|
||||
|
||||
try {
|
||||
await this.validateAndSave(url, loader)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
||||
import { DialogService, i18nKey, i18nPipe, sameUrl } from '@start9labs/shared'
|
||||
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
|
||||
@Injectable({
|
||||
@@ -16,14 +16,12 @@ export class MarketplaceAlertsService {
|
||||
url: string,
|
||||
originalUrl: string | null,
|
||||
): Promise<boolean> {
|
||||
const registries = await firstValueFrom(
|
||||
this.marketplaceService.getRegistries$(),
|
||||
)
|
||||
const registries = await firstValueFrom(this.marketplaceService.registries$)
|
||||
const message = originalUrl
|
||||
? `${this.i18n.transform('installed from')} ${registries.find(h => h.url === originalUrl) || originalUrl}`
|
||||
? `${this.i18n.transform('installed from')} ${registries.find(r => sameUrl(r.url, originalUrl))?.name || originalUrl}`
|
||||
: this.i18n.transform('sideloaded')
|
||||
|
||||
const currentName = registries.find(h => h.url === url) || url
|
||||
const currentName = registries.find(h => sameUrl(h.url, url))?.name || url
|
||||
|
||||
return new Promise(async resolve => {
|
||||
this.dialog
|
||||
|
||||
@@ -1,31 +1,107 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
@Component({
|
||||
selector: '[progress]',
|
||||
selector: 'service-install-progress',
|
||||
template: `
|
||||
<ng-content />
|
||||
@if (progress | installingProgress; as percent) {
|
||||
: {{ percent }}%
|
||||
<progress
|
||||
tuiProgressBar
|
||||
<header>
|
||||
{{ 'Install Progress' | i18n }}
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
[style.color]="
|
||||
progress === true
|
||||
? 'var(--tui-text-positive)'
|
||||
: 'var(--tui-text-action)'
|
||||
"
|
||||
[value]="percent / 100"
|
||||
></progress>
|
||||
appearance="primary-destructive"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="cancel()"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@for (
|
||||
phase of pkg.stateInfo.installingInfo?.progress?.phases;
|
||||
track $index
|
||||
) {
|
||||
<div *tuiLet="phase.progress | installingProgress as percent">
|
||||
{{ $any(phase.name) | i18n }}:
|
||||
@if (phase.progress === null) {
|
||||
<span>{{ 'waiting' | i18n }}</span>
|
||||
} @else if (phase.progress === true) {
|
||||
<span>{{ 'complete' | i18n }}!</span>
|
||||
} @else if (phase.progress === false || phase.progress.total === null) {
|
||||
<span>{{ 'in progress' | i18n }}...</span>
|
||||
} @else {
|
||||
<span>{{ percent }}%</span>
|
||||
}
|
||||
<progress
|
||||
tuiProgressBar
|
||||
size="m"
|
||||
[max]="100"
|
||||
[class.g-positive]="phase.progress === true"
|
||||
[value]="isPending(phase.progress) ? undefined : percent"
|
||||
></progress>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: [':host { line-height: 2rem }'],
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 6;
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
span {
|
||||
float: right;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
progress {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiProgress, InstallingProgressPipe],
|
||||
imports: [TuiProgress, TuiLet, InstallingProgressPipe, i18nPipe, TuiButton],
|
||||
})
|
||||
export class ServiceProgressComponent {
|
||||
@Input({ required: true }) progress!: T.Progress
|
||||
export class ServiceInstallProgressComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
|
||||
isPending(progress: T.Progress): boolean {
|
||||
return (
|
||||
!progress || (progress && progress !== true && progress.total === null)
|
||||
)
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
try {
|
||||
await this.api.cancelInstallPackage({ id: getManifest(this.pkg).id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
@if (loading) {
|
||||
<tui-loader size="s" />
|
||||
} @else {
|
||||
@if (healthy) {
|
||||
<tui-icon icon="@tui.check" class="g-positive" />
|
||||
} @else {
|
||||
@if (!healthy) {
|
||||
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,46 +25,45 @@ import { ServiceDependenciesComponent } from '../components/dependencies.compone
|
||||
import { ServiceErrorComponent } from '../components/error.component'
|
||||
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
|
||||
import { ServiceInterfacesComponent } from '../components/interfaces.component'
|
||||
import { ServiceProgressComponent } from '../components/progress.component'
|
||||
import { ServiceInstallProgressComponent } from '../components/progress.component'
|
||||
import { ServiceStatusComponent } from '../components/status.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<service-status
|
||||
[connected]="!!connected()"
|
||||
[installingInfo]="pkg()?.stateInfo?.installingInfo"
|
||||
[status]="status()"
|
||||
>
|
||||
@if ($any(pkg()?.status)?.started; as started) {
|
||||
<p class="g-secondary" [appUptime]="started"></p>
|
||||
}
|
||||
@if (installed() && connected() && pkg(); as pkg) {
|
||||
<service-controls [pkg]="pkg" [status]="status()" />
|
||||
}
|
||||
</service-status>
|
||||
@if (pkg(); as pkg) {
|
||||
@if (installing()) {
|
||||
<service-install-progress [pkg]="pkg" />
|
||||
} @else if (installed()) {
|
||||
<service-status
|
||||
[connected]="!!connected()"
|
||||
[installingInfo]="pkg.stateInfo.installingInfo"
|
||||
[status]="status()"
|
||||
>
|
||||
@if ($any(pkg.status)?.started; as started) {
|
||||
<p class="g-secondary" [appUptime]="started"></p>
|
||||
}
|
||||
|
||||
@if (installed() && pkg(); as pkg) {
|
||||
@if (pkg.status.main === 'error') {
|
||||
<service-error [pkg]="pkg" />
|
||||
}
|
||||
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||
@if (errors() | async; as errors) {
|
||||
<service-dependencies
|
||||
[pkg]="pkg"
|
||||
[services]="services()"
|
||||
[errors]="errors"
|
||||
/>
|
||||
}
|
||||
<service-health-checks [checks]="health()" />
|
||||
<service-action-requests [pkg]="pkg" [services]="services() || {}" />
|
||||
}
|
||||
@if (connected()) {
|
||||
<service-controls [pkg]="pkg" [status]="status()" />
|
||||
}
|
||||
</service-status>
|
||||
|
||||
@if (installing() && pkg(); as pkg) {
|
||||
@for (
|
||||
item of pkg.stateInfo.installingInfo?.progress?.phases;
|
||||
track $index
|
||||
) {
|
||||
<p [progress]="item.progress">{{ item.name }}</p>
|
||||
@if (pkg.status.main === 'error') {
|
||||
<service-error [pkg]="pkg" />
|
||||
}
|
||||
|
||||
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||
|
||||
@if (errors() | async; as errors) {
|
||||
<service-dependencies
|
||||
[pkg]="pkg"
|
||||
[services]="services()"
|
||||
[errors]="errors"
|
||||
/>
|
||||
}
|
||||
|
||||
<service-health-checks [checks]="health()" />
|
||||
<service-action-requests [pkg]="pkg" [services]="services() || {}" />
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -94,7 +93,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ServiceProgressComponent,
|
||||
ServiceInstallProgressComponent,
|
||||
ServiceStatusComponent,
|
||||
ServiceControlsComponent,
|
||||
ServiceInterfacesComponent,
|
||||
|
||||
@@ -27,13 +27,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
<span tuiSubtitle>
|
||||
@if (progress.complete) {
|
||||
<tui-icon icon="@tui.check" class="g-positive" />
|
||||
{{ 'Complete' | i18n }}
|
||||
{{ 'complete' | i18n }}
|
||||
} @else {
|
||||
@if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') {
|
||||
<tui-loader size="s" />
|
||||
{{ 'Backing up' | i18n }}
|
||||
{{ 'backing up' | i18n }}
|
||||
} @else {
|
||||
{{ 'Waiting' | i18n }}...
|
||||
{{ 'waiting' | i18n }}
|
||||
}
|
||||
}
|
||||
</span>
|
||||
|
||||
@@ -224,14 +224,12 @@ export default class UpdatesComponent {
|
||||
|
||||
readonly data = toSignal<UpdatesData>(
|
||||
combineLatest({
|
||||
hosts: this.marketplaceService
|
||||
.getRegistries$(true)
|
||||
.pipe(
|
||||
tap(
|
||||
([registry]) =>
|
||||
!this.isMobile && registry && this.current.set(registry),
|
||||
),
|
||||
hosts: this.marketplaceService.filteredRegistries$.pipe(
|
||||
tap(
|
||||
([registry]) =>
|
||||
!this.isMobile && registry && this.current.set(registry),
|
||||
),
|
||||
),
|
||||
marketplace: this.marketplaceService.marketplace$,
|
||||
localPkgs: inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData')
|
||||
@@ -248,7 +246,7 @@ export default class UpdatesComponent {
|
||||
),
|
||||
),
|
||||
),
|
||||
errors: this.marketplaceService.getRequestErrors$(),
|
||||
errors: this.marketplaceService.requestErrors$,
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -319,6 +319,9 @@ export namespace RR {
|
||||
export type InstallPackageReq = T.InstallParams
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type CancelInstallPackageReq = { id: string }
|
||||
export type CancelInstallPackageRes = null
|
||||
|
||||
export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input
|
||||
export type GetActionInputRes = {
|
||||
spec: IST.InputSpec
|
||||
|
||||
@@ -325,6 +325,10 @@ export abstract class ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes>
|
||||
|
||||
abstract cancelInstallPackage(
|
||||
params: RR.CancelInstallPackageReq,
|
||||
): Promise<RR.CancelInstallPackageRes>
|
||||
|
||||
abstract getActionInput(
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes>
|
||||
|
||||
@@ -560,6 +560,12 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async cancelInstallPackage(
|
||||
params: RR.CancelInstallPackageReq,
|
||||
): Promise<RR.CancelInstallPackageRes> {
|
||||
return this.rpcRequest({ method: 'package.cancel-install', params })
|
||||
}
|
||||
|
||||
async getActionInput(
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes> {
|
||||
|
||||
@@ -50,10 +50,7 @@ const PROGRESS: T.FullProgress = {
|
||||
},
|
||||
{
|
||||
name: 'Installing',
|
||||
progress: {
|
||||
done: 0,
|
||||
total: 40,
|
||||
},
|
||||
progress: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1077,6 +1074,22 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async cancelInstallPackage(
|
||||
params: RR.CancelInstallPackageReq,
|
||||
): Promise<RR.CancelInstallPackageRes> {
|
||||
await pauseFor(500)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.id}`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async getActionInput(
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes> {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
GetPackageRes,
|
||||
Marketplace,
|
||||
MarketplacePkg,
|
||||
StoreData,
|
||||
StoreDataWithUrl,
|
||||
StoreIdentity,
|
||||
} from '@start9labs/marketplace'
|
||||
import { Exver, defaultRegistries, sameUrl } from '@start9labs/shared'
|
||||
import { defaultRegistries, Exver, sameUrl } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
@@ -40,29 +39,11 @@ const { start9, community } = defaultRegistries
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MarketplaceService {
|
||||
private readonly currentRegistryUrlSubject$ = new ReplaySubject<string>(1)
|
||||
private readonly currentRegistryUrl$ = this.currentRegistryUrlSubject$.pipe(
|
||||
distinctUntilChanged(),
|
||||
)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly patch: PatchDB<DataModel> = inject(PatchDB)
|
||||
private readonly exver = inject(Exver)
|
||||
|
||||
private readonly currentRegistry$: Observable<StoreDataWithUrl> =
|
||||
this.currentRegistryUrl$.pipe(
|
||||
switchMap(url => this.fetchRegistry$(url)),
|
||||
filter(Boolean),
|
||||
map(registry => {
|
||||
registry.info.categories = {
|
||||
all: {
|
||||
name: 'All',
|
||||
},
|
||||
...registry.info.categories,
|
||||
}
|
||||
|
||||
return registry
|
||||
}),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly registries$: Observable<StoreIdentity[]> = this.patch
|
||||
readonly registries$: Observable<StoreIdentity[]> = this.patch
|
||||
.watch$('ui', 'registries')
|
||||
.pipe(
|
||||
map(registries => [
|
||||
@@ -74,21 +55,23 @@ export class MarketplaceService {
|
||||
]),
|
||||
)
|
||||
|
||||
private readonly filteredRegistries$: Observable<StoreIdentity[]> =
|
||||
combineLatest([
|
||||
this.clientStorageService.showDevTools$,
|
||||
this.registries$,
|
||||
]).pipe(
|
||||
map(([devMode, registries]) =>
|
||||
devMode
|
||||
? registries
|
||||
: registries.filter(
|
||||
({ url }) => !url.includes('alpha') && !url.includes('beta'),
|
||||
),
|
||||
),
|
||||
)
|
||||
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
|
||||
readonly filteredRegistries$: Observable<StoreIdentity[]> = combineLatest([
|
||||
inject(ClientStorageService).showDevTools$,
|
||||
this.registries$,
|
||||
]).pipe(
|
||||
map(([devMode, registries]) =>
|
||||
devMode
|
||||
? registries
|
||||
: registries.filter(
|
||||
({ url }) => !url.includes('alpha') && !url.includes('beta'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private readonly requestErrors$ = new BehaviorSubject<string[]>([])
|
||||
readonly currentRegistryUrl$ = new ReplaySubject<string>(1)
|
||||
|
||||
readonly requestErrors$ = new BehaviorSubject<string[]>([])
|
||||
|
||||
readonly marketplace$: Observable<Marketplace> = this.registries$.pipe(
|
||||
startWith<StoreIdentity[]>([]),
|
||||
@@ -102,11 +85,11 @@ export class MarketplaceService {
|
||||
if (data?.info.name)
|
||||
this.updateRegistryName(url, name, data.info.name)
|
||||
}),
|
||||
map<StoreData | null, [string, StoreData | null]>(data => [url, data]),
|
||||
startWith<[string, StoreData | null]>([url, null]),
|
||||
map(data => [url, data] satisfies [string, StoreDataWithUrl | null]),
|
||||
startWith<[string, StoreDataWithUrl | null]>([url, null]),
|
||||
),
|
||||
),
|
||||
scan<[string, StoreData | null], Record<string, StoreData | null>>(
|
||||
scan<[string, StoreDataWithUrl | null], Marketplace>(
|
||||
(requests, [url, store]) => {
|
||||
requests[url] = store
|
||||
|
||||
@@ -114,32 +97,21 @@ export class MarketplaceService {
|
||||
},
|
||||
{},
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly clientStorageService: ClientStorageService,
|
||||
private readonly exver: Exver,
|
||||
) {}
|
||||
|
||||
getRegistries$(filtered = false): Observable<StoreIdentity[]> {
|
||||
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
|
||||
return filtered ? this.filteredRegistries$ : this.registries$
|
||||
}
|
||||
|
||||
getCurrentRegistryUrl$() {
|
||||
return this.currentRegistryUrl$
|
||||
}
|
||||
|
||||
setRegistryUrl(url: string) {
|
||||
this.currentRegistryUrlSubject$.next(url)
|
||||
}
|
||||
|
||||
getCurrentRegistry$(): Observable<StoreDataWithUrl> {
|
||||
return this.currentRegistry$
|
||||
}
|
||||
readonly currentRegistry$: Observable<StoreDataWithUrl> = combineLatest([
|
||||
this.marketplace$,
|
||||
this.currentRegistryUrl$,
|
||||
this.currentRegistryUrl$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(url => this.fetchRegistry$(url).pipe(startWith(null))),
|
||||
),
|
||||
]).pipe(
|
||||
map(([all, url, current]) => current || all[url]),
|
||||
filter(Boolean),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
getPackage$(
|
||||
id: string,
|
||||
@@ -161,14 +133,12 @@ export class MarketplaceService {
|
||||
)
|
||||
}
|
||||
|
||||
fetchInfo$(url: string): Observable<T.RegistryInfo> {
|
||||
return from(this.api.getRegistryInfo({ registry: url })).pipe(
|
||||
fetchInfo$(registry: string): Observable<T.RegistryInfo> {
|
||||
return from(this.api.getRegistryInfo({ registry })).pipe(
|
||||
map(info => ({
|
||||
...info,
|
||||
categories: {
|
||||
all: {
|
||||
name: 'All',
|
||||
},
|
||||
all: { name: 'All' },
|
||||
...info.categories,
|
||||
},
|
||||
})),
|
||||
@@ -263,10 +233,6 @@ export class MarketplaceService {
|
||||
}
|
||||
}
|
||||
|
||||
getRequestErrors$(): Observable<string[]> {
|
||||
return this.requestErrors$
|
||||
}
|
||||
|
||||
async installPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
|
||||
Reference in New Issue
Block a user