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: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: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:setup": "ng run setup-wizard:build",
"build:ui": "ng run ui:build",

View File

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

View File

@@ -1,113 +1,108 @@
<header>
<ng-container *tuiLet="store$ | async as store">
<div class="title">
<store-icon
[class.tui-skeleton]="!store"
[class.tui-skeleton_rounded]="!store"
size="60px"
[url]="store?.url || ''"
[marketplace]="iconConfig"
/>
<h1 [class.tui-skeleton]="!store">
{{ store?.info?.name || 'Loading store...' }}
</h1>
<!-- change registry modal -->
<ng-content select="[slot=desktop]"></ng-content>
</div>
<!-- mobile nav -->
<div class="nav-mobile">
<div class="nav-mobile-bar">
<!-- 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 -->
<div class="title">
<store-icon
[class.tui-skeleton]="!registry"
[class.tui-skeleton_rounded]="!registry"
size="60px"
[url]="registry?.url || ''"
[marketplace]="iconConfig"
/>
<h1 [class.tui-skeleton]="!registry">
{{ registry?.info?.name || 'Unnamed Registry' }}
</h1>
<!-- change registry modal -->
<ng-content select="[slot=desktop]"></ng-content>
</div>
<!-- mobile nav -->
<div class="nav-mobile">
<div class="nav-mobile-bar">
<!-- mobile search -->
<marketplace-search
[query]="query"
[(query)]="query"
(queryChange)="onQueryChange($event)"
/>
<div class="nav-desktop-container">
<marketplace-categories
[categories]="store?.info?.categories"
[category]="query ? '' : category"
(categoryChange)="onCategoryChange($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]="registry?.url || ''"
[marketplace]="iconConfig"
[class.tui-skeleton]="!registry"
[class.tui-skeleton_rounded]="!registry"
/>
<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>
<nav
*tuiSidebar="open; direction: 'right'; autoWidth: true"
class="nav-mobile-sidebar divide-bar"
>
<div class="nav-mobile-sidebar-top">
<h1 [class.tui-skeleton]="!registry">
{{ registry?.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]="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>
</nav>
</ng-container>
</div>
</nav>
</header>

View File

@@ -5,11 +5,10 @@ import {
Input,
OnDestroy,
} 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 { Subject, takeUntil } from 'rxjs'
import { AbstractCategoryService } from '../../services/category.service'
import { StoreDataWithUrl } from '../../types'
@Component({
selector: 'menu',
@@ -21,19 +20,11 @@ export class MenuComponent implements OnDestroy {
@Input({ required: true })
iconConfig!: MarketplaceConfig
@Input({ required: true })
registry!: StoreDataWithUrl | null
private destroy$ = new Subject<void>()
private readonly marketplaceService = inject(AbstractMarketplaceService)
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 = ''
query = ''
open = false
@@ -52,13 +43,6 @@ export class MenuComponent implements OnDestroy {
.subscribe(val => {
this.category = val
})
this.marketplaceService
.getKnownHosts$()
.pipe(takeUntil(this.destroy$))
.subscribe(hosts => {
this.hosts = hosts
})
}
onCategoryChange(category: string): void {
@@ -66,7 +50,6 @@ export class MenuComponent implements OnDestroy {
this.query = ''
this.categoryService.resetQuery()
this.categoryService.changeCategory(category)
this.categoryService.handleNavigation()
}
onQueryChange(query: string): void {

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
import {
ChangeDetectionStrategy,
Component,
inject,
EventEmitter,
Input,
Output,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { CopyService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { CopyService, MarkdownComponent } from '@start9labs/shared'
import { MarketplacePkg } from '../../../types'
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
@Component({
selector: 'marketplace-additional',
@@ -21,7 +20,8 @@ export class AdditionalComponent {
@Input({ required: true })
pkg!: MarketplacePkg
private readonly marketplaceService = inject(AbstractMarketplaceService)
@Output()
readonly static = new EventEmitter<string>()
constructor(
readonly copyService: CopyService,
@@ -30,19 +30,4 @@ export class AdditionalComponent {
) {}
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
[routerLink]="[]"
[queryParams]="{ id: pkg.id, flavor: pkg.flavor }"
queryParamsHandling="merge"
>
<tui-avatar [src]="pkg.icon | trustUrl" />
<span tuiTitle>

View File

@@ -29,7 +29,6 @@ export * from './components/menu/menu.component.module'
export * from './components/menu/menu.component'
export * from './components/registry.component'
export * from './services/marketplace.service'
export * from './services/category.service'
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
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 {
AbstractCategoryService,
AbstractMarketplaceService,
FilterPackagesPipe,
} from '@start9labs/marketplace'
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 { DateTransformerService } from './services/date-transformer.service'
import { DatetimeTransformerService } from './services/datetime-transformer.service'
import { MarketplaceService } from './services/marketplace.service'
import { StorageService } from './services/storage.service'
import { ThemeSwitcherService } from './services/theme-switcher.service'
@@ -90,10 +88,6 @@ export const APP_PROVIDERS: Provider[] = [
provide: THEME,
useExisting: ThemeSwitcherService,
},
{
provide: AbstractMarketplaceService,
useClass: MarketplaceService,
},
{
provide: AbstractCategoryService,
useClass: CategoryService,

View File

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

View File

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

View File

@@ -163,6 +163,7 @@ export class MarketplaceTileComponent {
id: open ? this.pkg().id : 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 {
AbstractCategoryService,
AbstractMarketplaceService,
FilterPackagesPipe,
FilterPackagesPipeModule,
} from '@start9labs/marketplace'
import { combineLatest, map } from 'rxjs'
import { tap, withLatestFrom } from 'rxjs'
import { MarketplaceNotificationComponent } from './components/notification.component'
import { MarketplaceMenuComponent } from './components/menu.component'
import { MarketplaceTileComponent } from './components/tile.component'
import { MarketplaceControlsComponent } from './components/controls.component'
import { MarketplacePreviewComponent } from './modals/preview.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({
standalone: true,
@@ -21,15 +26,19 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
<tui-scrollbar>
<div class="marketplace-content-wrapper">
<div class="marketplace-content-inner">
<marketplace-notification [url]="(details$ | async)?.url || ''" />
<marketplace-notification [url]="(url$ | async) || ''" />
<div class="title-wrapper">
<h1>
{{ category$ | async | titlecase }}
</h1>
</div>
@if (filtered$ | async; as filtered) {
@if (registry$ | async; as registry) {
<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
[pkg]="pkg"
[style.--animation-order]="$index"
@@ -58,6 +67,7 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
padding: 0;
background: rgb(55 58 63 / 90%)
url('/assets/img/background_marketplace.png') no-repeat top right;
background-size: cover;
}
.marketplace-content {
@@ -127,6 +137,7 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
font-size: 1.25rem;
line-height: 1.75rem;
padding-left: 1.5rem;
font-weight: normal;
}
:host-context(tui-root._mobile) {
@@ -145,20 +156,34 @@ import { MarketplaceSidebarsComponent } from './components/sidebars.component'
MarketplacePreviewComponent,
MarketplaceSidebarsComponent,
TuiScrollbar,
FilterPackagesPipeModule,
],
})
export class MarketplaceComponent {
private readonly pipe = inject(FilterPackagesPipe)
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 filtered$ = combineLatest([
this.marketplaceService
.getSelectedStore$()
.pipe(map(({ packages }) => packages)),
this.categoryService.getQuery$(),
this.category$,
]).pipe(map(args => this.pipe.transform(...args)))
readonly query$ = this.categoryService.getQuery$()
readonly registry$ = this.marketplaceService.getRegistry$()
}

View File

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

View File

@@ -9,12 +9,20 @@ import {
toUrl,
} from '@start9labs/shared'
import {
AbstractMarketplaceService,
StoreIconComponentModule,
MarketplaceRegistryComponent,
} from '@start9labs/marketplace'
import { TuiDialogService, TuiIcon, TuiTitle, TuiButton } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import {
TuiDialogService,
TuiIcon,
TuiTitle,
TuiButton,
TuiDialogContext,
} from '@taiga-ui/core'
import {
PolymorpheusComponent,
POLYMORPHEUS_CONTEXT,
} from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { combineLatest, filter, firstValueFrom, map, Subscription } from 'rxjs'
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 { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry'
import { ConfigService } from 'src/app/services/config.service'
import { ActivatedRoute, Router } from '@angular/router'
@Component({
standalone: true,
@@ -90,9 +99,10 @@ export class MarketplaceRegistryModal {
private readonly errorService = inject(ErrorService)
private readonly formDialog = inject(FormDialogService)
private readonly dialogs = inject(TuiDialogService)
private readonly marketplace = inject(
AbstractMarketplaceService,
) as MarketplaceService
private readonly marketplaceService = inject(MarketplaceService)
private readonly context = inject<TuiDialogContext>(POLYMORPHEUS_CONTEXT)
private readonly route = inject(ActivatedRoute)
private readonly router = inject(Router)
private readonly hosts$ = inject<PatchDB<DataModel>>(PatchDB).watch$(
'ui',
'marketplace',
@@ -101,13 +111,13 @@ export class MarketplaceRegistryModal {
readonly marketplaceConfig = inject(ConfigService).marketplace
readonly stores$ = combineLatest([
this.marketplace.getKnownHosts$(),
this.marketplace.getSelectedHost$(),
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getRegistryUrl$(),
]).pipe(
map(([stores, selected]) =>
map(([stores, selectedUrl]) =>
stores.map(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
@@ -170,9 +180,14 @@ export class MarketplaceRegistryModal {
loader.unsubscribe()
loader.closed = false
loader.add(this.loader.open('Changing Registry...').subscribe())
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) {
this.errorService.handleError(e)
} finally {
@@ -210,7 +225,9 @@ export class MarketplaceRegistryModal {
loader.closed = false
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
loader.unsubscribe()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,65 @@
import { Injectable } from '@angular/core'
import {
AbstractMarketplaceService,
Marketplace,
StoreIdentity,
MarketplacePkg,
GetPackageRes,
StoreData,
StoreDataWithUrl,
} from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
import {
BehaviorSubject,
catchError,
combineLatest,
distinctUntilKeyChanged,
filter,
from,
map,
mergeMap,
Observable,
of,
scan,
pairwise,
shareReplay,
startWith,
switchMap,
take,
distinctUntilChanged,
ReplaySubject,
tap,
} from 'rxjs'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { ConfigService } from './config.service'
import { Exver, sameUrl } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { ClientStorageService } from './client-storage.service'
import { T } from '@start9labs/start-sdk'
@Injectable()
export class MarketplaceService implements AbstractMarketplaceService {
@Injectable({
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
.watch$('ui', 'marketplace', 'knownHosts')
.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[]>([])
constructor(
@@ -149,33 +107,17 @@ export class MarketplaceService implements AbstractMarketplaceService {
return filtered ? this.filteredKnownHosts$ : this.knownHosts$
}
getSelectedHost$(): Observable<StoreIdentity> {
return this.selectedHost$
getRegistryUrl$() {
return this.registryUrl$
}
getMarketplace$(filtered = false): Observable<Marketplace> {
// option to filter out hosts containing 'alpha' or 'beta' substrings in registryURL
return filtered ? this.filteredMarketplace$ : this.marketplace$
setRegistryUrl(url: string | null) {
const registryUrl = url || this.config.marketplace.start9
this.registryUrlSubject$.next(registryUrl)
}
getSelectedStore$(): Observable<StoreData> {
return this.selectedStore$
}
getSelectedStoreWithCategories$() {
return this.selectedHost$.pipe(
switchMap(({ url }) =>
this.marketplace$.pipe(
map(m => m[url]),
filter(Boolean),
map(({ info, packages }) => ({
url,
info,
packages,
})),
),
),
)
getRegistry$(): Observable<StoreDataWithUrl> {
return this.registry$
}
getPackage$(
@@ -184,47 +126,20 @@ export class MarketplaceService implements AbstractMarketplaceService {
flavor: string | null,
registryUrl?: string,
): Observable<MarketplacePkg> {
return this.selectedHost$.pipe(
switchMap(selected =>
this.marketplace$.pipe(
switchMap(m => {
const url = registryUrl || selected.url
const pkg = m[url]?.packages.find(
p =>
p.id === id &&
p.flavor === flavor &&
(!version || this.exver.compareExver(p.version, version) === 0),
)
return pkg ? of(pkg) : this.fetchPackage$(url, id, version, flavor)
}),
),
),
return this.registry$.pipe(
switchMap(registry => {
const url = registryUrl || registry.url
const pkg = registry.packages.find(
p =>
p.id === id &&
p.flavor === flavor &&
(!version || this.exver.compareExver(p.version, version) === 0),
)
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> {
return from(this.api.getRegistryInfo(url)).pipe(
map(info => ({
@@ -232,7 +147,10 @@ export class MarketplaceService implements AbstractMarketplaceService {
categories: {
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,
},
@@ -240,16 +158,17 @@ export class MarketplaceService implements AbstractMarketplaceService {
)
}
fetchStatic$(
getStatic$(
pkg: MarketplacePkg,
type: 'LICENSE.md' | 'instructions.md',
): Observable<string> {
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(
map(([info, packages]) => ({ info, packages })),
map(([info, packages]) => ({ info, packages, url })),
catchError(e => {
console.error(e)
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,
version: string | null | undefined,
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(
url: string,
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 {

View File

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

View File

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