mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -37,3 +37,5 @@ export type MarketplacePkg = T.PackageVersionInfo &
|
||||
version: string
|
||||
flavor: string | null
|
||||
}
|
||||
|
||||
export type StoreDataWithUrl = StoreData & { url: string }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -163,6 +163,7 @@ export class MarketplaceTileComponent {
|
||||
id: open ? this.pkg().id : null,
|
||||
flavor: open ? this.pkg().flavor : null,
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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$()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
|
||||
@@ -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))
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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$(),
|
||||
// })
|
||||
// }
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user