Update Marketplace (#2742)

* update abstract marketplace for usage accuracy andrename store to registry

* use new abstract functions

* fix(marketplace): get rid of `AbstractMarketplaceService`

* bump shared marketplace lib

* update marketplace to use query params for registry url; comment out updates page - will be refactored

* cleanup

* cleanup duplicate

* cleanup unused imports

* rework setting registry url when loading marketplace

* cleanup marketplace service

* fix background

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Lucy
2024-10-09 11:23:08 -06:00
committed by GitHub
parent a9569d0ed9
commit dfda2f7d5d
26 changed files with 688 additions and 807 deletions

View File

@@ -13,6 +13,7 @@
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
"build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)", "build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)",
"build:deps:win": "rimraf .angular/cache && (cd ../sdk && npm ci && npm run build) && (cd ../patch-db/client && npm ci && npm run build)",
"build:install": "ng run install-wizard:build", "build:install": "ng run install-wizard:build",
"build:setup": "ng run setup-wizard:build", "build:setup": "ng run setup-wizard:build",
"build:ui": "ng run ui:build", "build:ui": "ng run ui:build",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/marketplace", "name": "@start9labs/marketplace",
"version": "0.3.32", "version": "0.3.36",
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=13.2.0", "@angular/common": ">=13.2.0",
"@angular/core": ">=13.2.0", "@angular/core": ">=13.2.0",

View File

@@ -1,113 +1,108 @@
<header> <header>
<ng-container *tuiLet="store$ | async as store"> <div class="title">
<div class="title"> <store-icon
<store-icon [class.tui-skeleton]="!registry"
[class.tui-skeleton]="!store" [class.tui-skeleton_rounded]="!registry"
[class.tui-skeleton_rounded]="!store" size="60px"
size="60px" [url]="registry?.url || ''"
[url]="store?.url || ''" [marketplace]="iconConfig"
[marketplace]="iconConfig" />
/> <h1 [class.tui-skeleton]="!registry">
<h1 [class.tui-skeleton]="!store"> {{ registry?.info?.name || 'Unnamed Registry' }}
{{ store?.info?.name || 'Loading store...' }} </h1>
</h1> <!-- change registry modal -->
<!-- change registry modal --> <ng-content select="[slot=desktop]"></ng-content>
<ng-content select="[slot=desktop]"></ng-content> </div>
</div> <!-- mobile nav -->
<!-- mobile nav --> <div class="nav-mobile">
<div class="nav-mobile"> <div class="nav-mobile-bar">
<div class="nav-mobile-bar"> <!-- mobile search -->
<!-- mobile search -->
<marketplace-search
[(query)]="query"
(queryChange)="onQueryChange($event)"
/>
<button
tuiButton
type="button"
appearance="link"
(click)="toggleMenu(true)"
(tuiActiveZoneChange)="toggleMenu($event)"
[style.--tui-padding]="'1.2rem'"
>
<store-icon
size="42px"
[style.height]="'42px'"
[style.border-radius]="'100%'"
[url]="store?.url || ''"
[marketplace]="iconConfig"
[class.tui-skeleton]="!store"
[class.tui-skeleton_rounded]="!store"
/>
<nav
*tuiSidebar="open; direction: 'right'; autoWidth: true"
class="nav-mobile-sidebar divide-bar"
>
<div class="nav-mobile-sidebar-top">
<h1 [class.tui-skeleton]="!store">
{{ store?.info?.name }}
</h1>
<button
[style.--tui-padding]="0"
tuiButton
type="button"
appearance="icon"
iconStart="@tui.x"
(tuiActiveZoneChange)="toggleMenu($event)"
(click)="toggleMenu(false)"
></button>
</div>
<!-- change registry modal -->
<ng-content select="[slot=mobile]"></ng-content>
<div class="nav-mobile-sidebar-bottom divide-bar">
<marketplace-categories
[categories]="store?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event); toggleMenu(false)"
/>
<div>
<!-- link to store for brochure -->
<ng-content select="[slot=store-mobile]" />
<a
target="_blank"
rel="noreferrer"
href="https://docs.start9.com/0.3.5.x/developer-docs/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a>
</div>
</div>
</nav>
</button>
</div>
</div>
<!-- desktop nav -->
<nav class="nav-desktop">
<!-- desktop search -->
<marketplace-search <marketplace-search
[query]="query" [(query)]="query"
(queryChange)="onQueryChange($event)" (queryChange)="onQueryChange($event)"
/> />
<div class="nav-desktop-container"> <button
<marketplace-categories tuiButton
[categories]="store?.info?.categories" type="button"
[category]="query ? '' : category" appearance="link"
(categoryChange)="onCategoryChange($event)" (click)="toggleMenu(true)"
(tuiActiveZoneChange)="toggleMenu($event)"
[style.--tui-padding]="'1.2rem'"
>
<store-icon
size="42px"
[style.height]="'42px'"
[style.border-radius]="'100%'"
[url]="registry?.url || ''"
[marketplace]="iconConfig"
[class.tui-skeleton]="!registry"
[class.tui-skeleton_rounded]="!registry"
/> />
<div> <nav
<!-- link to store for brochure --> *tuiSidebar="open; direction: 'right'; autoWidth: true"
<ng-content select="[slot=store]" /> class="nav-mobile-sidebar divide-bar"
<a >
target="_blank" <div class="nav-mobile-sidebar-top">
rel="noreferrer" <h1 [class.tui-skeleton]="!registry">
href="https://docs.start9.com/0.3.5.x/developer-docs/" {{ registry?.info?.name }}
> </h1>
<span>Package a service</span> <button
<tui-icon tuiAppearance="icon" icon="@tui.external-link" /> [style.--tui-padding]="0"
</a> tuiButton
</div> type="button"
appearance="icon"
iconStart="@tui.x"
(tuiActiveZoneChange)="toggleMenu($event)"
(click)="toggleMenu(false)"
></button>
</div>
<!-- change registry modal -->
<ng-content select="[slot=mobile]"></ng-content>
<div class="nav-mobile-sidebar-bottom divide-bar">
<marketplace-categories
[categories]="registry?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event); toggleMenu(false)"
/>
<div>
<!-- link to store for brochure -->
<ng-content select="[slot=store-mobile]" />
<a
target="_blank"
rel="noreferrer"
href="https://docs.start9.com/0.3.5.x/developer-docs/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a>
</div>
</div>
</nav>
</button>
</div>
</div>
<!-- desktop nav -->
<nav class="nav-desktop">
<!-- desktop search -->
<marketplace-search [query]="query" (queryChange)="onQueryChange($event)" />
<div class="nav-desktop-container">
<marketplace-categories
[categories]="registry?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($event)"
/>
<div>
<!-- link to store for brochure -->
<ng-content select="[slot=store]" />
<a
target="_blank"
rel="noreferrer"
href="https://docs.start9.com/0.3.5.x/developer-docs/"
>
<span>Package a service</span>
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
</a>
</div> </div>
</nav> </div>
</ng-container> </nav>
</header> </header>

View File

@@ -5,11 +5,10 @@ import {
Input, Input,
OnDestroy, OnDestroy,
} from '@angular/core' } from '@angular/core'
import { combineLatest, map, Subject, takeUntil } from 'rxjs'
import { StoreIdentity } from '../../types'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
import { AbstractCategoryService } from '../../services/category.service'
import { MarketplaceConfig } from '@start9labs/shared' import { MarketplaceConfig } from '@start9labs/shared'
import { Subject, takeUntil } from 'rxjs'
import { AbstractCategoryService } from '../../services/category.service'
import { StoreDataWithUrl } from '../../types'
@Component({ @Component({
selector: 'menu', selector: 'menu',
@@ -21,19 +20,11 @@ export class MenuComponent implements OnDestroy {
@Input({ required: true }) @Input({ required: true })
iconConfig!: MarketplaceConfig iconConfig!: MarketplaceConfig
@Input({ required: true })
registry!: StoreDataWithUrl | null
private destroy$ = new Subject<void>() private destroy$ = new Subject<void>()
private readonly marketplaceService = inject(AbstractMarketplaceService)
private readonly categoryService = inject(AbstractCategoryService) private readonly categoryService = inject(AbstractCategoryService)
readonly store$ = this.marketplaceService.getSelectedStoreWithCategories$()
readonly alt$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getSelectedHost$(),
]).pipe(
map(([stores, selected]) =>
stores.filter(({ url }) => url != selected.url),
),
)
private hosts?: StoreIdentity[]
category = '' category = ''
query = '' query = ''
open = false open = false
@@ -52,13 +43,6 @@ export class MenuComponent implements OnDestroy {
.subscribe(val => { .subscribe(val => {
this.category = val this.category = val
}) })
this.marketplaceService
.getKnownHosts$()
.pipe(takeUntil(this.destroy$))
.subscribe(hosts => {
this.hosts = hosts
})
} }
onCategoryChange(category: string): void { onCategoryChange(category: string): void {
@@ -66,7 +50,6 @@ export class MenuComponent implements OnDestroy {
this.query = '' this.query = ''
this.categoryService.resetQuery() this.categoryService.resetQuery()
this.categoryService.changeCategory(category) this.categoryService.changeCategory(category)
this.categoryService.handleNavigation()
} }
onQueryChange(query: string): void { onQueryChange(query: string): void {

View File

@@ -1,6 +1,5 @@
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { MarketplacePkg } from '../../src/types'
import { Exver, MarkdownPipeModule } from '@start9labs/shared' import { Exver, MarkdownPipeModule } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core' import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core'
import { TuiAccordion } from '@taiga-ui/kit' import { TuiAccordion } from '@taiga-ui/kit'
@@ -8,26 +7,21 @@ import {
POLYMORPHEUS_CONTEXT, POLYMORPHEUS_CONTEXT,
PolymorpheusComponent, PolymorpheusComponent,
} from '@taiga-ui/polymorpheus' } from '@taiga-ui/polymorpheus'
import { map } from 'rxjs' import { MarketplacePkg } from '../../src/types'
import { AbstractMarketplaceService } from '../services/marketplace.service'
@Component({ @Component({
standalone: true, standalone: true,
template: ` template: `
@if (notes$ | async; as notes) { <tui-accordion>
<tui-accordion> @for (note of notes | keyvalue: asIsOrder; track $index) {
@for (note of notes | keyvalue: asIsOrder; track $index) { <tui-accordion-item>
<tui-accordion-item> {{ note.key }}
{{ note.key }} <ng-template tuiAccordionItemContent>
<ng-template tuiAccordionItemContent> <div [innerHTML]="note.value | markdown"></div>
<div [innerHTML]="note.value | markdown"></div> </ng-template>
</ng-template> </tui-accordion-item>
</tui-accordion-item> }
} </tui-accordion>
</tui-accordion>
} @else {
<tui-loader textContent="Loading Release Notes" />
}
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
@@ -43,26 +37,20 @@ export class ReleaseNotesComponent {
private readonly pkg = private readonly pkg =
inject<TuiDialogContext<void, MarketplacePkg>>(POLYMORPHEUS_CONTEXT).data inject<TuiDialogContext<void, MarketplacePkg>>(POLYMORPHEUS_CONTEXT).data
readonly notes$ = inject(AbstractMarketplaceService) readonly notes = Object.entries(this.pkg.otherVersions)
.getSelectedStore$() .filter(
.pipe( ([v, _]) =>
map(s => { this.exver.getFlavor(v) === this.pkg.flavor &&
return Object.entries(this.pkg.otherVersions) this.exver.compareExver(this.pkg.version, v) === 1,
.filter( )
([v, _]) => .reduce(
this.exver.getFlavor(v) === this.pkg.flavor && (obj, [version, info]) => ({
this.exver.compareExver(this.pkg.version, v) === 1, ...obj,
) [version]: info.releaseNotes,
.reduce(
(obj, [version, info]) => ({
...obj,
[version]: info.releaseNotes,
}),
{
[`${this.pkg.version} (current)`]: this.pkg.releaseNotes,
},
)
}), }),
{
[`${this.pkg.version} (current)`]: this.pkg.releaseNotes,
},
) )
asIsOrder(a: any, b: any) { asIsOrder(a: any, b: any) {

View File

@@ -28,7 +28,7 @@
</ng-template> </ng-template>
<!-- license --> <!-- license -->
<marketplace-additional-item <marketplace-additional-item
(click)="presentModalMd('License')" (click)="static.emit('License')"
[data]="pkg.license" [data]="pkg.license"
label="License" label="License"
icon="@tui.chevron-right" icon="@tui.chevron-right"
@@ -36,7 +36,7 @@
/> />
<!-- instructions --> <!-- instructions -->
<marketplace-additional-item <marketplace-additional-item
(click)="presentModalMd('Instructions')" (click)="static.emit('Instructions')"
data="Click to view instructions" data="Click to view instructions"
label="Instructions" label="Instructions"
icon="@tui.chevron-right" icon="@tui.chevron-right"

View File

@@ -1,15 +1,14 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
inject, EventEmitter,
Input, Input,
Output,
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { CopyService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { CopyService, MarkdownComponent } from '@start9labs/shared'
import { MarketplacePkg } from '../../../types' import { MarketplacePkg } from '../../../types'
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
@Component({ @Component({
selector: 'marketplace-additional', selector: 'marketplace-additional',
@@ -21,7 +20,8 @@ export class AdditionalComponent {
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkg
private readonly marketplaceService = inject(AbstractMarketplaceService) @Output()
readonly static = new EventEmitter<string>()
constructor( constructor(
readonly copyService: CopyService, readonly copyService: CopyService,
@@ -30,19 +30,4 @@ export class AdditionalComponent {
) {} ) {}
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
presentModalMd(label: string) {
this.dialogs
.open(new PolymorpheusComponent(MarkdownComponent), {
label,
size: 'l',
data: {
content: this.marketplaceService.fetchStatic$(
this.pkg,
label === 'License' ? 'LICENSE.md' : 'instructions.md',
),
},
})
.subscribe()
}
} }

View File

@@ -18,6 +18,7 @@ import { MarketplacePkg } from '../../../types'
tuiCell tuiCell
[routerLink]="[]" [routerLink]="[]"
[queryParams]="{ id: pkg.id, flavor: pkg.flavor }" [queryParams]="{ id: pkg.id, flavor: pkg.flavor }"
queryParamsHandling="merge"
> >
<tui-avatar [src]="pkg.icon | trustUrl" /> <tui-avatar [src]="pkg.icon | trustUrl" />
<span tuiTitle> <span tuiTitle>

View File

@@ -29,7 +29,6 @@ export * from './components/menu/menu.component.module'
export * from './components/menu/menu.component' export * from './components/menu/menu.component'
export * from './components/registry.component' export * from './components/registry.component'
export * from './services/marketplace.service'
export * from './services/category.service' export * from './services/category.service'
export * from './types' export * from './types'

View File

@@ -1,28 +0,0 @@
import { Observable } from 'rxjs'
import { Marketplace, MarketplacePkg, StoreData, StoreIdentity } from '../types'
export abstract class AbstractMarketplaceService {
abstract getKnownHosts$(): Observable<StoreIdentity[]>
abstract getSelectedHost$(): Observable<StoreIdentity>
abstract getMarketplace$(): Observable<Marketplace>
abstract getSelectedStore$(): Observable<StoreData>
abstract getSelectedStoreWithCategories$(): Observable<
StoreIdentity & StoreData
>
abstract getPackage$(
id: string,
version: string | null,
flavor: string | null,
url?: string,
): Observable<MarketplacePkg>
abstract fetchStatic$(
pkg: MarketplacePkg,
type: 'LICENSE.md' | 'instructions.md',
): Observable<string>
}

View File

@@ -37,3 +37,5 @@ export type MarketplacePkg = T.PackageVersionInfo &
version: string version: string
flavor: string | null flavor: string | null
} }
export type StoreDataWithUrl = StoreData & { url: string }

View File

@@ -3,7 +3,6 @@ import { UntypedFormBuilder } from '@angular/forms'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import {
AbstractCategoryService, AbstractCategoryService,
AbstractMarketplaceService,
FilterPackagesPipe, FilterPackagesPipe,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared' import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared'
@@ -35,7 +34,6 @@ import { CategoryService } from './services/category.service'
import { ClientStorageService } from './services/client-storage.service' import { ClientStorageService } from './services/client-storage.service'
import { DateTransformerService } from './services/date-transformer.service' import { DateTransformerService } from './services/date-transformer.service'
import { DatetimeTransformerService } from './services/datetime-transformer.service' import { DatetimeTransformerService } from './services/datetime-transformer.service'
import { MarketplaceService } from './services/marketplace.service'
import { StorageService } from './services/storage.service' import { StorageService } from './services/storage.service'
import { ThemeSwitcherService } from './services/theme-switcher.service' import { ThemeSwitcherService } from './services/theme-switcher.service'
@@ -90,10 +88,6 @@ export const APP_PROVIDERS: Provider[] = [
provide: THEME, provide: THEME,
useExisting: ThemeSwitcherService, useExisting: ThemeSwitcherService,
}, },
{
provide: AbstractMarketplaceService,
useClass: MarketplaceService,
},
{ {
provide: AbstractCategoryService, provide: AbstractCategoryService,
useClass: CategoryService, useClass: CategoryService,

View File

@@ -7,10 +7,7 @@ import {
Input, Input,
} from '@angular/core' } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import { MarketplacePkg } from '@start9labs/marketplace'
AbstractMarketplaceService,
MarketplacePkg,
} from '@start9labs/marketplace'
import { import {
Exver, Exver,
ErrorService, ErrorService,
@@ -106,12 +103,7 @@ export class MarketplaceControlsComponent {
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly exver = inject(Exver) private readonly exver = inject(Exver)
private readonly router = inject(Router) private readonly router = inject(Router)
private readonly marketplace = inject( private readonly marketplaceService = inject(MarketplaceService)
AbstractMarketplaceService,
) as MarketplaceService
@Input()
url?: string
@Input({ required: true }) @Input({ required: true })
pkg!: MarketplacePkg pkg!: MarketplacePkg
@@ -125,19 +117,19 @@ export class MarketplaceControlsComponent {
readonly showDevTools$ = inject(ClientStorageService).showDevTools$ readonly showDevTools$ = inject(ClientStorageService).showDevTools$
async tryInstall() { async tryInstall() {
const current = await firstValueFrom(this.marketplace.getSelectedHost$()) const currentUrl = await firstValueFrom(
const url = this.url || current.url this.marketplaceService.getRegistryUrl$(),
)
const originalUrl = this.localPkg?.registry || '' const originalUrl = this.localPkg?.registry || ''
if (!this.localPkg) { if (!this.localPkg) {
if (await this.alerts.alertInstall(this.pkg)) this.install(url) if (await this.alerts.alertInstall(this.pkg)) this.install(currentUrl)
return return
} }
if ( if (
!sameUrl(url, originalUrl) && !sameUrl(currentUrl, originalUrl) &&
!(await this.alerts.alertMarketplace(url, originalUrl)) !(await this.alerts.alertMarketplace(currentUrl, originalUrl))
) { ) {
return return
} }
@@ -148,9 +140,9 @@ export class MarketplaceControlsComponent {
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) && hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
this.exver.compareExver(localManifest.version, this.pkg.version) !== 0 this.exver.compareExver(localManifest.version, this.pkg.version) !== 0
) { ) {
this.dryInstall(url) this.dryInstall(currentUrl)
} else { } else {
this.install(url) this.install(currentUrl)
} }
} }
@@ -178,7 +170,7 @@ export class MarketplaceControlsComponent {
const { id, version } = this.pkg const { id, version } = this.pkg
try { try {
await this.marketplace.installPackage(id, version, url) await this.marketplaceService.installPackage(id, version, url)
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {

View File

@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { MenuModule } from '@start9labs/marketplace' import { MenuModule } from '@start9labs/marketplace'
import { import {
TuiDialogService, TuiDialogService,
@@ -8,12 +9,13 @@ import {
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { MARKETPLACE_REGISTRY } from '../modals/registry.component' import { MARKETPLACE_REGISTRY } from '../modals/registry.component'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Component({ @Component({
standalone: true, standalone: true,
selector: 'marketplace-menu', selector: 'marketplace-menu',
template: ` template: `
<menu [iconConfig]="marketplace"> <menu [iconConfig]="marketplace" [registry]="registry$ | async">
<button <button
slot="desktop" slot="desktop"
tuiIconButton tuiIconButton
@@ -45,11 +47,13 @@ import { MARKETPLACE_REGISTRY } from '../modals/registry.component'
`, `,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MenuModule, TuiButton, TuiIcon, TuiAppearance], imports: [CommonModule, MenuModule, TuiButton, TuiIcon, TuiAppearance],
}) })
export class MarketplaceMenuComponent { export class MarketplaceMenuComponent {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
readonly marketplace = inject(ConfigService).marketplace readonly marketplace = inject(ConfigService).marketplace
private readonly marketplaceService = inject(MarketplaceService)
readonly registry$ = this.marketplaceService.getRegistry$()
changeRegistry() { changeRegistry() {
this.dialogs this.dialogs

View File

@@ -163,6 +163,7 @@ export class MarketplaceTileComponent {
id: open ? this.pkg().id : null, id: open ? this.pkg().id : null,
flavor: open ? this.pkg().flavor : null, flavor: open ? this.pkg().flavor : null,
}, },
queryParamsHandling: 'merge',
}) })
} }
} }

View File

@@ -3,16 +3,21 @@ import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { import {
AbstractCategoryService, AbstractCategoryService,
AbstractMarketplaceService,
FilterPackagesPipe, FilterPackagesPipe,
FilterPackagesPipeModule,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { combineLatest, map } from 'rxjs' import { tap, withLatestFrom } from 'rxjs'
import { MarketplaceNotificationComponent } from './components/notification.component' import { MarketplaceNotificationComponent } from './components/notification.component'
import { MarketplaceMenuComponent } from './components/menu.component' import { MarketplaceMenuComponent } from './components/menu.component'
import { MarketplaceTileComponent } from './components/tile.component' import { MarketplaceTileComponent } from './components/tile.component'
import { MarketplaceControlsComponent } from './components/controls.component' import { MarketplaceControlsComponent } from './components/controls.component'
import { MarketplacePreviewComponent } from './modals/preview.component' import { MarketplacePreviewComponent } from './modals/preview.component'
import { MarketplaceSidebarsComponent } from './components/sidebars.component' import { MarketplaceSidebarsComponent } from './components/sidebars.component'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { ActivatedRoute, Router } from '@angular/router'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({ @Component({
standalone: true, standalone: true,
@@ -21,15 +26,19 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
<tui-scrollbar> <tui-scrollbar>
<div class="marketplace-content-wrapper"> <div class="marketplace-content-wrapper">
<div class="marketplace-content-inner"> <div class="marketplace-content-inner">
<marketplace-notification [url]="(details$ | async)?.url || ''" /> <marketplace-notification [url]="(url$ | async) || ''" />
<div class="title-wrapper"> <div class="title-wrapper">
<h1> <h1>
{{ category$ | async | titlecase }} {{ category$ | async | titlecase }}
</h1> </h1>
</div> </div>
@if (filtered$ | async; as filtered) { @if (registry$ | async; as registry) {
<section class="marketplace-content-list"> <section class="marketplace-content-list">
@for (pkg of filtered; track $index) { @for (
pkg of registry.packages
| filterPackages: (query$ | async) : (category$ | async);
track $index
) {
<marketplace-tile <marketplace-tile
[pkg]="pkg" [pkg]="pkg"
[style.--animation-order]="$index" [style.--animation-order]="$index"
@@ -58,6 +67,7 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
padding: 0; padding: 0;
background: rgb(55 58 63 / 90%) background: rgb(55 58 63 / 90%)
url('/assets/img/background_marketplace.png') no-repeat top right; url('/assets/img/background_marketplace.png') no-repeat top right;
background-size: cover;
} }
.marketplace-content { .marketplace-content {
@@ -127,6 +137,7 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
padding-left: 1.5rem; padding-left: 1.5rem;
font-weight: normal;
} }
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
@@ -145,20 +156,34 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
MarketplacePreviewComponent, MarketplacePreviewComponent,
MarketplaceSidebarsComponent, MarketplaceSidebarsComponent,
TuiScrollbar, TuiScrollbar,
FilterPackagesPipeModule,
], ],
}) })
export class MarketplaceComponent { export class MarketplaceComponent {
private readonly pipe = inject(FilterPackagesPipe)
private readonly categoryService = inject(AbstractCategoryService) private readonly categoryService = inject(AbstractCategoryService)
private readonly marketplaceService = inject(AbstractMarketplaceService) private readonly marketplaceService = inject(MarketplaceService)
private readonly router = inject(Router)
private readonly patch = inject(PatchDB<DataModel>)
private readonly route = inject(ActivatedRoute)
.queryParamMap.pipe(
takeUntilDestroyed(),
withLatestFrom(this.patch.watch$('ui', 'marketplace', 'selectedUrl')),
tap(([params, selectedUrl]) => {
const registry = params.get('registry')
if (!registry) {
this.router.navigate([], {
queryParams: { registry: selectedUrl },
queryParamsHandling: 'merge',
})
} else {
this.marketplaceService.setRegistryUrl(registry)
}
}),
)
.subscribe()
readonly details$ = this.marketplaceService.getSelectedHost$() readonly url$ = this.marketplaceService.getRegistryUrl$()
readonly category$ = this.categoryService.getCategory$() readonly category$ = this.categoryService.getCategory$()
readonly filtered$ = combineLatest([ readonly query$ = this.categoryService.getQuery$()
this.marketplaceService readonly registry$ = this.marketplaceService.getRegistry$()
.getSelectedStore$()
.pipe(map(({ packages }) => packages)),
this.categoryService.getQuery$(),
this.category$,
]).pipe(map(args => this.pipe.transform(...args)))
} }

View File

@@ -10,7 +10,6 @@ import { FormsModule } from '@angular/forms'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import {
AboutModule, AboutModule,
AbstractMarketplaceService,
AdditionalModule, AdditionalModule,
FlavorsComponent, FlavorsComponent,
MarketplaceAdditionalItemComponent, MarketplaceAdditionalItemComponent,
@@ -35,6 +34,7 @@ import {
startWith, startWith,
switchMap, switchMap,
} from 'rxjs' } from 'rxjs'
import { MarketplaceService } from 'src/app/services/marketplace.service'
@Component({ @Component({
selector: 'marketplace-preview', selector: 'marketplace-preview',
@@ -186,8 +186,8 @@ export class MarketplacePreviewComponent {
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
private readonly exver = inject(Exver) private readonly exver = inject(Exver)
private readonly router = inject(Router) private readonly router = inject(Router)
private readonly marketplaceService = inject(AbstractMarketplaceService) private readonly marketplaceService = inject(MarketplaceService)
private readonly version$ = new BehaviorSubject<string>('*') private readonly version$ = new BehaviorSubject<string | null>(null)
private readonly flavor$ = this.router.routerState.root.queryParamMap.pipe( private readonly flavor$ = this.router.routerState.root.queryParamMap.pipe(
map(paramMap => paramMap.get('flavor')), map(paramMap => paramMap.get('flavor')),
) )
@@ -202,7 +202,7 @@ export class MarketplacePreviewComponent {
readonly flavors$ = this.flavor$.pipe( readonly flavors$ = this.flavor$.pipe(
switchMap(current => switchMap(current =>
this.marketplaceService.getSelectedStore$().pipe( this.marketplaceService.getRegistry$().pipe(
map(({ packages }) => map(({ packages }) =>
packages.filter( packages.filter(
({ id, flavor }) => id === this.pkgId && flavor !== current, ({ id, flavor }) => id === this.pkgId && flavor !== current,

View File

@@ -9,12 +9,20 @@ import {
toUrl, toUrl,
} from '@start9labs/shared' } from '@start9labs/shared'
import { import {
AbstractMarketplaceService,
StoreIconComponentModule, StoreIconComponentModule,
MarketplaceRegistryComponent, MarketplaceRegistryComponent,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { TuiDialogService, TuiIcon, TuiTitle, TuiButton } from '@taiga-ui/core' import {
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' TuiDialogService,
TuiIcon,
TuiTitle,
TuiButton,
TuiDialogContext,
} from '@taiga-ui/core'
import {
PolymorpheusComponent,
POLYMORPHEUS_CONTEXT,
} from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { combineLatest, filter, firstValueFrom, map, Subscription } from 'rxjs' import { combineLatest, filter, firstValueFrom, map, Subscription } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component' import { FormComponent } from 'src/app/routes/portal/components/form.component'
@@ -24,6 +32,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry' import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { ActivatedRoute, Router } from '@angular/router'
@Component({ @Component({
standalone: true, standalone: true,
@@ -90,9 +99,10 @@ export class MarketplaceRegistryModal {
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly formDialog = inject(FormDialogService) private readonly formDialog = inject(FormDialogService)
private readonly dialogs = inject(TuiDialogService) private readonly dialogs = inject(TuiDialogService)
private readonly marketplace = inject( private readonly marketplaceService = inject(MarketplaceService)
AbstractMarketplaceService, private readonly context = inject<TuiDialogContext>(POLYMORPHEUS_CONTEXT)
) as MarketplaceService private readonly route = inject(ActivatedRoute)
private readonly router = inject(Router)
private readonly hosts$ = inject<PatchDB<DataModel>>(PatchDB).watch$( private readonly hosts$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui', 'ui',
'marketplace', 'marketplace',
@@ -101,13 +111,13 @@ export class MarketplaceRegistryModal {
readonly marketplaceConfig = inject(ConfigService).marketplace readonly marketplaceConfig = inject(ConfigService).marketplace
readonly stores$ = combineLatest([ readonly stores$ = combineLatest([
this.marketplace.getKnownHosts$(), this.marketplaceService.getKnownHosts$(),
this.marketplace.getSelectedHost$(), this.marketplaceService.getRegistryUrl$(),
]).pipe( ]).pipe(
map(([stores, selected]) => map(([stores, selectedUrl]) =>
stores.map(s => ({ stores.map(s => ({
...s, ...s,
selected: sameUrl(s.url, selected.url), selected: sameUrl(s.url, selectedUrl),
})), })),
), ),
// 0 and 1 are prod and community, 2 and beyond are alts // 0 and 1 are prod and community, 2 and beyond are alts
@@ -170,9 +180,14 @@ export class MarketplaceRegistryModal {
loader.unsubscribe() loader.unsubscribe()
loader.closed = false loader.closed = false
loader.add(this.loader.open('Changing Registry...').subscribe()) loader.add(this.loader.open('Changing Registry...').subscribe())
try { try {
await this.api.setDbValue<string>(['marketplace', 'selectedUrl'], url) this.marketplaceService.setRegistryUrl(url)
this.router.navigate([], {
queryParams: { registry: url },
queryParamsHandling: 'merge',
})
this.api.setDbValue<string>(['marketplace', 'selectedUrl'], url)
this.context.$implicit.complete()
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {
@@ -210,7 +225,9 @@ export class MarketplaceRegistryModal {
loader.closed = false loader.closed = false
loader.add(this.loader.open('Validating marketplace...').subscribe()) loader.add(this.loader.open('Validating marketplace...').subscribe())
const { name } = await firstValueFrom(this.marketplace.fetchInfo$(url)) const { name } = await firstValueFrom(
this.marketplaceService.fetchInfo$(url),
)
// Save // Save
loader.unsubscribe() loader.unsubscribe()

View File

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

View File

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

View File

@@ -1,202 +1,199 @@
import { Component, inject, Input } from '@angular/core' // import { Component, inject, Input } from '@angular/core'
import { RouterLink } from '@angular/router' // import { RouterLink } from '@angular/router'
import { // import {
AbstractMarketplaceService, // MarketplacePkg,
MarketplacePkg, // } from '@start9labs/marketplace'
} from '@start9labs/marketplace' // import {
import { // MarkdownPipeModule,
MarkdownPipeModule, // SafeLinksDirective,
SafeLinksDirective, // SharedPipesModule,
SharedPipesModule, // } from '@start9labs/shared'
} from '@start9labs/shared' // import {
import { // TuiDialogService,
TuiDialogService, // TuiLoader,
TuiLoader, // TuiIcon,
TuiIcon, // TuiLink,
TuiLink, // TuiButton,
TuiButton, // } from '@taiga-ui/core'
} from '@taiga-ui/core' // import {
import { // TuiProgress,
TuiProgress, // TuiAccordion,
TuiAccordion, // TuiAvatar,
TuiAvatar, // TUI_CONFIRM,
TUI_CONFIRM, // } from '@taiga-ui/kit'
} from '@taiga-ui/kit' // import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
import { NgDompurifyModule } from '@tinkoff/ng-dompurify' // import { PatchDB } from 'patch-db-client'
import { PatchDB } from 'patch-db-client' // import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe'
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/service/pipes/install-progress.pipe' // import { MarketplaceService } from 'src/app/services/marketplace.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' // import {
import { // DataModel,
DataModel, // InstalledState,
InstalledState, // PackageDataEntry,
PackageDataEntry, // UpdatingState,
UpdatingState, // } from 'src/app/services/patch-db/data-model'
} from 'src/app/services/patch-db/data-model' // import { getAllPackages } from 'src/app/utils/get-package-data'
import { getAllPackages } from 'src/app/utils/get-package-data' // import { hasCurrentDeps } from 'src/app/utils/has-deps'
import { hasCurrentDeps } from 'src/app/utils/has-deps'
@Component({ // @Component({
selector: 'updates-item', // selector: 'updates-item',
template: ` // template: `
<tui-accordion-item borders="top-bottom"> // <tui-accordion-item borders="top-bottom">
<div class="g-action"> // <div class="g-action">
<tui-avatar size="s"> // <tui-avatar size="s">
<img alt="" [src]="marketplacePkg.icon" /> // <img alt="" [src]="marketplacePkg.icon" />
</tui-avatar> // </tui-avatar>
<div [style.flex]="1" [style.overflow]="'hidden'"> // <div [style.flex]="1" [style.overflow]="'hidden'">
<strong>{{ marketplacePkg.title }}</strong> // <strong>{{ marketplacePkg.title }}</strong>
<div> // <div>
{{ localPkg.stateInfo.manifest.version }} // {{ localPkg.stateInfo.manifest.version }}
<tui-icon icon="@tui.arrow-right" [style.font-size.rem]="1" /> // <tui-icon icon="@tui.arrow-right" [style.font-size.rem]="1" />
<span [style.color]="'var(--tui-text-positive)'"> // <span [style.color]="'var(--tui-text-positive)'">
{{ marketplacePkg.version }} // {{ marketplacePkg.version }}
</span> // </span>
</div> // </div>
<div [style.color]="'var(--tui-text-negative)'">{{ errors }}</div> // <div [style.color]="'var(--tui-text-negative)'">{{ errors }}</div>
</div> // </div>
@if (localPkg.stateInfo.state === 'updating') { // @if (localPkg.stateInfo.state === 'updating') {
<tui-progress-circle // <tui-progress-circle
class="g-success" // class="g-success"
size="s" // size="s"
[max]="1" // [max]="1"
[value]=" // [value]="
(localPkg.stateInfo.installingInfo.progress.overall // (localPkg.stateInfo.installingInfo.progress.overall
| installingProgress) || 0 // | installingProgress) || 0
" // "
/> // />
} @else { // } @else {
@if (ready) { // @if (ready) {
<button // <button
tuiButton // tuiButton
size="s" // size="s"
[appearance]="errors ? 'destructive' : 'primary'" // [appearance]="errors ? 'destructive' : 'primary'"
(click.stop)="onClick()" // (click.stop)="onClick()"
> // >
{{ errors ? 'Retry' : 'Update' }} // {{ errors ? 'Retry' : 'Update' }}
</button> // </button>
} @else { // } @else {
<tui-loader [style.width.rem]="2" [inheritColor]="true" /> // <tui-loader [style.width.rem]="2" [inheritColor]="true" />
} // }
} // }
</div> // </div>
<ng-template tuiAccordionItemContent> // <ng-template tuiAccordionItemContent>
<strong>What's new</strong> // <strong>What's new</strong>
<p // <p
safeLinks // safeLinks
[innerHTML]="marketplacePkg.releaseNotes | markdown | dompurify" // [innerHTML]="marketplacePkg.releaseNotes | markdown | dompurify"
></p> // ></p>
<a // <a
tuiLink // tuiLink
iconEnd="@tui.external-link" // iconEnd="@tui.external-link"
routerLink="/marketplace" // routerLink="/marketplace"
[queryParams]="{ url: url, id: marketplacePkg.id }" // [queryParams]="{ url: url, id: marketplacePkg.id }"
> // >
View listing // View listing
</a> // </a>
</ng-template> // </ng-template>
</tui-accordion-item> // </tui-accordion-item>
`, // `,
styles: [ // styles: [
` // `
:host { // :host {
display: block; // display: block;
--tui-background-neutral-1-hover: transparent; // --tui-background-neutral-1-hover: transparent;
&:not(:last-child) { // &:not(:last-child) {
border-bottom: 1px solid var(--tui-background-neutral-1); // border-bottom: 1px solid var(--tui-background-neutral-1);
} // }
} // }
`, // `,
], // ],
standalone: true, // standalone: true,
imports: [ // imports: [
RouterLink, // RouterLink,
MarkdownPipeModule, // MarkdownPipeModule,
NgDompurifyModule, // NgDompurifyModule,
SafeLinksDirective, // SafeLinksDirective,
SharedPipesModule, // SharedPipesModule,
TuiProgress, // TuiProgress,
TuiAccordion, // TuiAccordion,
TuiAvatar, // TuiAvatar,
TuiIcon, // TuiIcon,
TuiButton, // TuiButton,
TuiLink, // TuiLink,
TuiLoader, // TuiLoader,
InstallingProgressPipe, // InstallingProgressPipe,
], // ],
}) // })
export class UpdatesItemComponent { // export class UpdatesItemComponent {
private readonly dialogs = inject(TuiDialogService) // private readonly dialogs = inject(TuiDialogService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) // private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly marketplace = inject( // private readonly marketplaceService = inject(MarketplaceService)
AbstractMarketplaceService,
) as MarketplaceService
@Input({ required: true }) // @Input({ required: true })
marketplacePkg!: MarketplacePkg // marketplacePkg!: MarketplacePkg
@Input({ required: true }) // @Input({ required: true })
localPkg!: PackageDataEntry<InstalledState | UpdatingState> // localPkg!: PackageDataEntry<InstalledState | UpdatingState>
@Input({ required: true }) // @Input({ required: true })
url!: string // url!: string
get pkgId(): string { // get pkgId(): string {
return this.marketplacePkg.id // return this.marketplacePkg.id
} // }
get errors(): string { // get errors(): string {
return this.marketplace.updateErrors[this.pkgId] // return this.marketplaceService.updateErrors[this.pkgId]
} // }
get ready(): boolean { // get ready(): boolean {
return !this.marketplace.updateQueue[this.pkgId] // return !this.marketplaceService.updateQueue[this.pkgId]
} // }
async onClick() { // async onClick() {
const { id } = this.marketplacePkg // const { id } = this.marketplacePkg
delete this.marketplace.updateErrors[id] // delete this.marketplaceService.updateErrors[id]
this.marketplace.updateQueue[id] = true // this.marketplaceService.updateQueue[id] = true
if (hasCurrentDeps(id, await getAllPackages(this.patch))) { // if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
const proceed = await this.alert() // const proceed = await this.alert()
if (proceed) { // if (proceed) {
await this.update() // await this.update()
} else { // } else {
delete this.marketplace.updateQueue[id] // delete this.marketplaceService.updateQueue[id]
} // }
} else { // } else {
await this.update() // await this.update()
} // }
} // }
private async update() { // private async update() {
const { id, version } = this.marketplacePkg // const { id, version } = this.marketplacePkg
try { // try {
await this.marketplace.installPackage(id, version, this.url) // await this.marketplaceService.installPackage(id, version, this.url)
delete this.marketplace.updateQueue[id] // delete this.marketplaceService.updateQueue[id]
} catch (e: any) { // } catch (e: any) {
delete this.marketplace.updateQueue[id] // delete this.marketplaceService.updateQueue[id]
this.marketplace.updateErrors[id] = e.message // this.marketplaceService.updateErrors[id] = e.message
} // }
} // }
private async alert(): Promise<boolean> { // private async alert(): Promise<boolean> {
return new Promise(async resolve => { // return new Promise(async resolve => {
this.dialogs // this.dialogs
.open<boolean>(TUI_CONFIRM, { // .open<boolean>(TUI_CONFIRM, {
label: 'Warning', // label: 'Warning',
size: 's', // size: 's',
data: { // data: {
content: `Services that depend on ${this.localPkg.stateInfo.manifest.title} will no longer work properly and may crash`, // content: `Services that depend on ${this.localPkg.stateInfo.manifest.title} will no longer work properly and may crash`,
yes: 'Continue', // yes: 'Continue',
no: 'Cancel', // no: 'Cancel',
}, // },
}) // })
.subscribe(response => resolve(response)) // .subscribe(response => resolve(response))
}) // })
} // }
} // }

View File

@@ -1,98 +1,95 @@
import { CommonModule } from '@angular/common' // import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' // import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { // import {
AbstractMarketplaceService, // StoreIconComponentModule,
StoreIconComponentModule, // } from '@start9labs/marketplace'
} from '@start9labs/marketplace' // import { TuiAvatar } from '@taiga-ui/kit'
import { TuiAvatar } from '@taiga-ui/kit' // import { TuiCell } from '@taiga-ui/layout'
import { TuiCell } from '@taiga-ui/layout' // import { PatchDB } from 'patch-db-client'
import { PatchDB } from 'patch-db-client' // import { combineLatest, map } from 'rxjs'
import { combineLatest, map } from 'rxjs' // import { FilterUpdatesPipe } from 'src/app/routes/portal/routes/system/updates/filter-updates.pipe'
import { FilterUpdatesPipe } from 'src/app/routes/portal/routes/system/updates/filter-updates.pipe' // import { UpdatesItemComponent } from 'src/app/routes/portal/routes/system/updates/item.component'
import { UpdatesItemComponent } from 'src/app/routes/portal/routes/system/updates/item.component' // import { ConfigService } from 'src/app/services/config.service'
import { ConfigService } from 'src/app/services/config.service' // import { MarketplaceService } from 'src/app/services/marketplace.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' // import {
import { // DataModel,
DataModel, // InstalledState,
InstalledState, // PackageDataEntry,
PackageDataEntry, // UpdatingState,
UpdatingState, // } from 'src/app/services/patch-db/data-model'
} from 'src/app/services/patch-db/data-model' // import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
import { isInstalled, isUpdating } from 'src/app/utils/get-package-data'
@Component({ // @Component({
template: ` // template: `
@if (data$ | async; as data) { // @if (data$ | async; as data) {
@for (host of data.hosts; track host) { // @for (host of data.hosts; track host) {
<h3 class="g-title"> // <h3 class="g-title">
<store-icon [url]="host.url" [marketplace]="mp" size="26px" /> // <store-icon [url]="host.url" [marketplace]="mp" size="26px" />
{{ host.name }} // {{ host.name }}
</h3> // </h3>
@if (data.errors.includes(host.url)) { // @if (data.errors.includes(host.url)) {
<p class="g-error">Request Failed</p> // <p class="g-error">Request Failed</p>
} // }
@if (data.mp[host.url]?.packages | filterUpdates: data.local; as pkgs) { // @if (data.mp[host.url]?.packages | filterUpdates: data.local; as pkgs) {
@for (pkg of pkgs; track pkg) { // @for (pkg of pkgs; track pkg) {
<updates-item // <updates-item
[marketplacePkg]="pkg" // [marketplacePkg]="pkg"
[localPkg]="data.local[pkg.id]" // [localPkg]="data.local[pkg.id]"
[url]="host.url" // [url]="host.url"
/> // />
} @empty { // } @empty {
<p>All services are up to date!</p> // <p>All services are up to date!</p>
} // }
} @else { // } @else {
@for (i of [0, 1, 2]; track i) { // @for (i of [0, 1, 2]; track i) {
<section tuiCell> // <section tuiCell>
<tui-avatar class="tui-skeleton" /> // <tui-avatar class="tui-skeleton" />
<span class="tui-skeleton">Loading update item</span> // <span class="tui-skeleton">Loading update item</span>
<span class="tui-skeleton" [style.margin-left]="'auto'"> // <span class="tui-skeleton" [style.margin-left]="'auto'">
Loading actions // Loading actions
</span> // </span>
</section> // </section>
} // }
} // }
} // }
} // }
`, // `,
host: { class: 'g-page' }, // host: { class: 'g-page' },
changeDetection: ChangeDetectionStrategy.OnPush, // changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, // standalone: true,
imports: [ // imports: [
CommonModule, // CommonModule,
TuiCell, // TuiCell,
TuiAvatar, // TuiAvatar,
StoreIconComponentModule, // StoreIconComponentModule,
FilterUpdatesPipe, // FilterUpdatesPipe,
UpdatesItemComponent, // UpdatesItemComponent,
], // ],
}) // })
export default class UpdatesComponent { // export default class UpdatesComponent {
private readonly service = inject( // private readonly marketplaceService = inject(MarketplaceService)
AbstractMarketplaceService,
) as MarketplaceService
readonly mp = inject(ConfigService).marketplace // readonly mp = inject(ConfigService).marketplace
readonly data$ = combineLatest({ // readonly data$ = combineLatest({
hosts: this.service.getKnownHosts$(true), // hosts: this.marketplaceService.getKnownHosts$(true),
mp: this.service.getMarketplace$(), // mp: this.marketplaceService.getMarketplace$(),
local: inject<PatchDB<DataModel>>(PatchDB) // local: inject<PatchDB<DataModel>>(PatchDB)
.watch$('packageData') // .watch$('packageData')
.pipe( // .pipe(
map(pkgs => // map(pkgs =>
Object.entries(pkgs).reduce( // Object.entries(pkgs).reduce(
(acc, [id, val]) => { // (acc, [id, val]) => {
if (isInstalled(val) || isUpdating(val)) // if (isInstalled(val) || isUpdating(val))
return { ...acc, [id]: val } // return { ...acc, [id]: val }
return acc // return acc
}, // },
{} as Record< // {} as Record<
string, // string,
PackageDataEntry<InstalledState | UpdatingState> // PackageDataEntry<InstalledState | UpdatingState>
>, // >,
), // ),
), // ),
), // ),
errors: this.service.getRequestErrors$(), // errors: this.marketplaceService.getRequestErrors$(),
}) // })
} // }

View File

@@ -1,5 +1,4 @@
import { inject, Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { Exver } from '@start9labs/shared' import { Exver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
@@ -10,7 +9,6 @@ import {
map, map,
Observable, Observable,
pairwise, pairwise,
shareReplay,
startWith, startWith,
switchMap, switchMap,
} from 'rxjs' } from 'rxjs'
@@ -32,9 +30,7 @@ export class BadgeService {
this.patch.watch$('serverInfo', 'ntpSynced'), this.patch.watch$('serverInfo', 'ntpSynced'),
inject(EOSService).updateAvailable$, inject(EOSService).updateAvailable$,
]).pipe(map(([synced, update]) => Number(!synced) + Number(update))) ]).pipe(map(([synced, update]) => Number(!synced) + Number(update)))
private readonly marketplace = inject( private readonly marketplaceService = inject(MarketplaceService)
AbstractMarketplaceService,
) as MarketplaceService
private readonly local$ = inject(ConnectionService).pipe( private readonly local$ = inject(ConnectionService).pipe(
filter(Boolean), filter(Boolean),
@@ -58,35 +54,35 @@ export class BadgeService {
), ),
) )
private readonly updates$ = combineLatest([ // private readonly updates$ = combineLatest([
this.marketplace.getMarketplace$(true), // this.marketplaceService.getMarketplace$(true),
this.local$, // this.local$,
]).pipe( // ]).pipe(
map( // map(
([marketplace, local]) => // ([marketplace, local]) =>
Object.entries(marketplace).reduce( // Object.entries(marketplace).reduce(
(list, [_, store]) => // (list, [_, store]) =>
store?.packages.reduce( // store?.packages.reduce(
(result, { id, version }) => // (result, { id, version }) =>
local[id] && // local[id] &&
this.exver.compareExver( // this.exver.compareExver(
version, // version,
getManifest(local[id]).version, // getManifest(local[id]).version,
) === 1 // ) === 1
? result.add(id) // ? result.add(id)
: result, // : result,
list, // list,
) || list, // ) || list,
new Set<string>(), // new Set<string>(),
).size, // ).size,
), // ),
shareReplay(1), // shareReplay(1),
) // )
getCount(id: string): Observable<number> { getCount(id: string): Observable<number> {
switch (id) { switch (id) {
case '/portal/system/updates': // case '/portal/system/updates':
return this.updates$ // return this.updates$
case '/portal/system/settings': case '/portal/system/settings':
return this.settings$ return this.settings$
case '/portal/system/notifications': case '/portal/system/notifications':

View File

@@ -1,42 +1,65 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { import {
AbstractMarketplaceService,
Marketplace,
StoreIdentity, StoreIdentity,
MarketplacePkg, MarketplacePkg,
GetPackageRes, GetPackageRes,
StoreData, StoreDataWithUrl,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
BehaviorSubject, BehaviorSubject,
catchError, catchError,
combineLatest, combineLatest,
distinctUntilKeyChanged,
filter, filter,
from, from,
map, map,
mergeMap,
Observable, Observable,
of, of,
scan,
pairwise,
shareReplay, shareReplay,
startWith,
switchMap, switchMap,
take, distinctUntilChanged,
ReplaySubject,
tap, tap,
} from 'rxjs' } from 'rxjs'
import { RR } from 'src/app/services/api/api.types' import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { Exver, sameUrl } from '@start9labs/shared' import { Exver } from '@start9labs/shared'
import { ClientStorageService } from './client-storage.service' import { ClientStorageService } from './client-storage.service'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
@Injectable() @Injectable({
export class MarketplaceService implements AbstractMarketplaceService { providedIn: 'root',
})
export class MarketplaceService {
private readonly registryUrlSubject$ = new ReplaySubject<string>(1)
private readonly registryUrl$ = this.registryUrlSubject$.pipe(
distinctUntilChanged(),
)
private readonly registry$: Observable<StoreDataWithUrl> =
this.registryUrl$.pipe(
switchMap(url => this.fetchRegistry$(url)),
filter(Boolean),
// @TODO is updateStoreName needed?
map(registry => {
registry.info.categories = {
all: {
name: 'All',
description: {
short: 'All registry packages',
long: 'An unfiltered list of all packages available on this registry.',
},
},
...registry.info.categories,
}
return registry
}),
shareReplay(1),
)
private readonly knownHosts$: Observable<StoreIdentity[]> = this.patch private readonly knownHosts$: Observable<StoreIdentity[]> = this.patch
.watch$('ui', 'marketplace', 'knownHosts') .watch$('ui', 'marketplace', 'knownHosts')
.pipe( .pipe(
@@ -69,71 +92,6 @@ export class MarketplaceService implements AbstractMarketplaceService {
), ),
) )
private readonly selectedHost$: Observable<StoreIdentity> = this.patch
.watch$('ui', 'marketplace')
.pipe(
distinctUntilKeyChanged('selectedUrl'),
map(({ selectedUrl: url, knownHosts: hosts }) =>
toStoreIdentity(url, hosts[url]),
),
shareReplay(1),
)
private readonly 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.fetchStore$(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(1),
)
private readonly filteredMarketplace$ = combineLatest([
this.clientStorageService.showDevTools$,
this.marketplace$,
]).pipe(
map(([devMode, marketplace]) =>
Object.entries(marketplace).reduce(
(filtered, [url, store]) =>
!devMode && (url.includes('alpha') || url.includes('beta'))
? filtered
: {
[url]: store,
...filtered,
},
{} as Marketplace,
),
),
)
private readonly selectedStore$: Observable<StoreData> =
this.selectedHost$.pipe(
switchMap(({ url }) =>
this.marketplace$.pipe(
map(m => m[url]),
filter(Boolean),
take(1),
),
),
)
private readonly requestErrors$ = new BehaviorSubject<string[]>([]) private readonly requestErrors$ = new BehaviorSubject<string[]>([])
constructor( constructor(
@@ -149,33 +107,17 @@ export class MarketplaceService implements AbstractMarketplaceService {
return filtered ? this.filteredKnownHosts$ : this.knownHosts$ return filtered ? this.filteredKnownHosts$ : this.knownHosts$
} }
getSelectedHost$(): Observable<StoreIdentity> { getRegistryUrl$() {
return this.selectedHost$ return this.registryUrl$
} }
getMarketplace$(filtered = false): Observable<Marketplace> { setRegistryUrl(url: string | null) {
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL const registryUrl = url || this.config.marketplace.start9
return filtered ? this.filteredMarketplace$ : this.marketplace$ this.registryUrlSubject$.next(registryUrl)
} }
getSelectedStore$(): Observable<StoreData> { getRegistry$(): Observable<StoreDataWithUrl> {
return this.selectedStore$ return this.registry$
}
getSelectedStoreWithCategories$() {
return this.selectedHost$.pipe(
switchMap(({ url }) =>
this.marketplace$.pipe(
map(m => m[url]),
filter(Boolean),
map(({ info, packages }) => ({
url,
info,
packages,
})),
),
),
)
} }
getPackage$( getPackage$(
@@ -184,47 +126,20 @@ export class MarketplaceService implements AbstractMarketplaceService {
flavor: string | null, flavor: string | null,
registryUrl?: string, registryUrl?: string,
): Observable<MarketplacePkg> { ): Observable<MarketplacePkg> {
return this.selectedHost$.pipe( return this.registry$.pipe(
switchMap(selected => switchMap(registry => {
this.marketplace$.pipe( const url = registryUrl || registry.url
switchMap(m => { const pkg = registry.packages.find(
const url = registryUrl || selected.url p =>
const pkg = m[url]?.packages.find( p.id === id &&
p => p.flavor === flavor &&
p.id === id && (!version || this.exver.compareExver(p.version, version) === 0),
p.flavor === flavor && )
(!version || this.exver.compareExver(p.version, version) === 0), return pkg ? of(pkg) : this.fetchPackage$(url, id, version, flavor)
) }),
return pkg ? of(pkg) : this.fetchPackage$(url, id, version, flavor)
}),
),
),
) )
} }
// UI only
readonly updateErrors: Record<string, string> = {}
readonly updateQueue: Record<string, boolean> = {}
getRequestErrors$(): Observable<string[]> {
return this.requestErrors$
}
async installPackage(
id: string,
version: string,
url: string,
): Promise<void> {
const params: RR.InstallPackageReq = {
id,
version,
registry: url,
}
await this.api.installPackage(params)
}
fetchInfo$(url: string): Observable<T.RegistryInfo> { fetchInfo$(url: string): Observable<T.RegistryInfo> {
return from(this.api.getRegistryInfo(url)).pipe( return from(this.api.getRegistryInfo(url)).pipe(
map(info => ({ map(info => ({
@@ -232,7 +147,10 @@ export class MarketplaceService implements AbstractMarketplaceService {
categories: { categories: {
all: { all: {
name: 'All', name: 'All',
description: { short: 'All services', long: 'All services' }, description: {
short: 'All services',
long: 'An unfiltered list of all services available on this registry.',
},
}, },
...info.categories, ...info.categories,
}, },
@@ -240,16 +158,17 @@ export class MarketplaceService implements AbstractMarketplaceService {
) )
} }
fetchStatic$( getStatic$(
pkg: MarketplacePkg, pkg: MarketplacePkg,
type: 'LICENSE.md' | 'instructions.md', type: 'LICENSE.md' | 'instructions.md',
): Observable<string> { ): Observable<string> {
return from(this.api.getStaticProxy(pkg, type)) return from(this.api.getStaticProxy(pkg, type))
} }
private fetchStore$(url: string): Observable<StoreData | null> { private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> {
console.log('FETCHING REGISTRY: ', url)
return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe( return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe(
map(([info, packages]) => ({ info, packages })), map(([info, packages]) => ({ info, packages, url })),
catchError(e => { catchError(e => {
console.error(e) console.error(e)
this.requestErrors$.next(this.requestErrors$.value.concat(url)) this.requestErrors$.next(this.requestErrors$.value.concat(url))
@@ -275,7 +194,22 @@ export class MarketplaceService implements AbstractMarketplaceService {
) )
} }
convertToMarketplacePkg( private fetchPackage$(
url: string,
id: string,
version: string | null,
flavor: string | null,
): Observable<MarketplacePkg> {
return from(
this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
).pipe(
map(pkgInfo =>
this.convertToMarketplacePkg(id, version, flavor, pkgInfo),
),
)
}
private convertToMarketplacePkg(
id: string, id: string,
version: string | null | undefined, version: string | null | undefined,
flavor: string | null, flavor: string | null,
@@ -297,26 +231,6 @@ export class MarketplaceService implements AbstractMarketplaceService {
} }
} }
private fetchPackage$(
url: string,
id: string,
version: string | null,
flavor: string | null,
): Observable<MarketplacePkg> {
return from(
this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
).pipe(
map(pkgInfo =>
this.convertToMarketplacePkg(
id,
version === '*' ? null : version,
flavor,
pkgInfo,
),
),
)
}
private async updateStoreName( private async updateStoreName(
url: string, url: string,
oldName: string | undefined, oldName: string | undefined,
@@ -329,6 +243,28 @@ export class MarketplaceService implements AbstractMarketplaceService {
) )
} }
} }
// UI only
readonly updateErrors: Record<string, string> = {}
readonly updateQueue: Record<string, boolean> = {}
getRequestErrors$(): Observable<string[]> {
return this.requestErrors$
}
async installPackage(
id: string,
version: string,
url: string,
): Promise<void> {
const params: RR.InstallPackageReq = {
id,
version,
registry: url,
}
await this.api.installPackage(params)
}
} }
function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity { function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity {

View File

@@ -1,14 +1,12 @@
import { Inject, Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { TuiDialogService } from '@taiga-ui/core' import { TuiDialogService } from '@taiga-ui/core'
import { filter, share, switchMap, take, Observable, map } from 'rxjs' import { filter, share, switchMap, Observable, map } from 'rxjs'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { EOSService } from 'src/app/services/eos.service' import { EOSService } from 'src/app/services/eos.service'
import { WelcomeComponent } from 'src/app/components/welcome.component' import { WelcomeComponent } from 'src/app/components/welcome.component'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
@@ -40,8 +38,6 @@ export class PatchDataService extends Observable<void> {
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly dialogs: TuiDialogService, private readonly dialogs: TuiDialogService,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly connection$: ConnectionService, private readonly connection$: ConnectionService,
private readonly bootstrapper: LocalStorageBootstrap, private readonly bootstrapper: LocalStorageBootstrap,
) { ) {
@@ -50,7 +46,7 @@ export class PatchDataService extends Observable<void> {
private checkForUpdates(): void { private checkForUpdates(): void {
this.eosService.loadEos() this.eosService.loadEos()
this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe() // this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe()
} }
private showEosWelcome(ackVersion: string) { private showEosWelcome(ackVersion: string) {

View File

@@ -12,10 +12,10 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
icon: '@tui.shopping-cart', icon: '@tui.shopping-cart',
title: 'Marketplace', title: 'Marketplace',
}, },
'/portal/system/updates': { // '/portal/system/updates': {
icon: '@tui.globe', // icon: '@tui.globe',
title: 'Updates', // title: 'Updates',
}, // },
'/portal/system/sideload': { '/portal/system/sideload': {
icon: '@tui.upload', icon: '@tui.upload',
title: 'Sideload', title: 'Sideload',