mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Feat/community marketplace (#1790)
* add community marketplace * Update embassy-mock-api.service.ts * expect ui/marketplace to be undefined * possible undefined from getpackage * fix marketplace pages * rework marketplace infrastructure * fix bugs Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>
This commit is contained in:
committed by
Aiden McClelland
parent
e2db3d84d8
commit
9998ed177b
@@ -9,10 +9,6 @@
|
||||
"mocks": {
|
||||
"maskAs": "tor",
|
||||
"skipStartupAlerts": true
|
||||
},
|
||||
"marketplace": {
|
||||
"url": "https://registry.start9.com/",
|
||||
"name": "Start9 Marketplace"
|
||||
}
|
||||
},
|
||||
"gitHash": ""
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": null,
|
||||
"auto-check-updates": true,
|
||||
"pkg-order": [],
|
||||
"ack-welcome": "0.3.2.1",
|
||||
"ack-welcome": "0.3.3",
|
||||
"marketplace": {
|
||||
"selected-id": null,
|
||||
"known-hosts": {}
|
||||
"selected-url": "https://registry.start9.com/",
|
||||
"known-hosts": {
|
||||
"https://registry.start9.com/": "Start9 Marketplace",
|
||||
"https://community-registry.start9.com/": "Community Marketplace"
|
||||
}
|
||||
},
|
||||
"dev": {},
|
||||
"gaming": {
|
||||
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
})
|
||||
export class CategoriesComponent {
|
||||
@Input()
|
||||
categories = new Set<string>()
|
||||
categories!: Set<string>
|
||||
|
||||
@Input()
|
||||
category = ''
|
||||
category!: string
|
||||
|
||||
@Input()
|
||||
updatesAvailable = 0
|
||||
updatesAvailable!: number
|
||||
|
||||
@Output()
|
||||
readonly categoryChange = new EventEmitter<string>()
|
||||
|
||||
@@ -14,7 +14,7 @@ export class ReleaseNotesComponent {
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
|
||||
readonly notes$ = this.marketplaceService.fetchReleaseNotes(this.pkgId)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@angular/core'
|
||||
import { AlertController, ModalController } from '@ionic/angular'
|
||||
import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared'
|
||||
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||
|
||||
@@ -58,9 +57,9 @@ export class AdditionalComponent {
|
||||
}
|
||||
|
||||
async presentModalMd(title: string) {
|
||||
const content = this.marketplaceService.getPackageMarkdown(
|
||||
title,
|
||||
const content = this.marketplaceService.fetchPackageMarkdown(
|
||||
this.pkg.manifest.id,
|
||||
title,
|
||||
)
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
<div class="text">
|
||||
<h1 class="title">{{ pkg.manifest.title }}</h1>
|
||||
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
|
||||
<!-- @TODO remove conditional when registry code deployed. published-at will be required -->
|
||||
<p *ngIf="pkg['published-at']" class="published">
|
||||
<p class="published">
|
||||
Released: {{ pkg['published-at'] | date: 'medium' }}
|
||||
</p>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
import { MarketplaceManifest } from '../types/marketplace-manifest'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterPackages',
|
||||
|
||||
@@ -26,7 +26,6 @@ export * from './pipes/filter-packages.pipe'
|
||||
export * from './services/marketplace.service'
|
||||
|
||||
export * from './types/dependency'
|
||||
export * from './types/marketplace'
|
||||
export * from './types/marketplace-data'
|
||||
export * from './types/marketplace-info'
|
||||
export * from './types/marketplace-manifest'
|
||||
export * from './types/marketplace-pkg'
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { MarketplaceInfo } from '../types/marketplace-info'
|
||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||
import { Marketplace } from '../types/marketplace'
|
||||
|
||||
export abstract class AbstractMarketplaceService {
|
||||
abstract getMarketplace(): Observable<Marketplace>
|
||||
abstract getMarketplaceInfo$(): Observable<MarketplaceInfo>
|
||||
|
||||
abstract getReleaseNotes(id: string): Observable<Record<string, string>>
|
||||
|
||||
abstract getCategories(): Observable<Set<string>>
|
||||
|
||||
abstract getPackages(): Observable<MarketplacePkg[]>
|
||||
|
||||
abstract getPackageMarkdown(type: string, pkgId: string): Observable<string>
|
||||
abstract getPackages$(): Observable<MarketplacePkg[]>
|
||||
|
||||
abstract getPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
): Observable<MarketplacePkg | null>
|
||||
url?: string,
|
||||
): Observable<MarketplacePkg | undefined>
|
||||
|
||||
abstract fetchReleaseNotes(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>>
|
||||
|
||||
abstract fetchPackageMarkdown(
|
||||
id: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
): Observable<string>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface MarketplaceData {
|
||||
categories: string[]
|
||||
export interface MarketplaceInfo {
|
||||
name: string
|
||||
categories: string[]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface Marketplace {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export class EmverComparesPipe implements PipeTransform {
|
||||
try {
|
||||
return this.emver.compare(first, second) as SemverResult
|
||||
} catch (e) {
|
||||
console.warn(`emver comparison failed`, e, first, second)
|
||||
console.error(`emver comparison failed`, e, first, second)
|
||||
return 'comparison-impossible'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ export class HttpService {
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly http: HttpClient,
|
||||
) {
|
||||
const { protocol, hostname, port } = this.document.location
|
||||
this.fullUrl = `${protocol}//${hostname}:${port}`
|
||||
this.fullUrl = this.document.location.origin
|
||||
}
|
||||
|
||||
async rpcRequest<T>(
|
||||
|
||||
@@ -12,9 +12,5 @@ export type WorkspaceConfig = {
|
||||
maskAs: 'tor' | 'lan'
|
||||
skipStartupAlerts: boolean
|
||||
}
|
||||
marketplace: {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { EOSService } from '../../services/eos.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { iif, Observable } from 'rxjs'
|
||||
import { filter, map, switchMap } from 'rxjs/operators'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
@@ -47,9 +47,21 @@ export class MenuComponent {
|
||||
|
||||
readonly showEOSUpdate$ = this.eosService.showUpdate$
|
||||
|
||||
readonly updateCount$: Observable<number> = this.marketplaceService
|
||||
.getUpdates()
|
||||
.pipe(map(pkgs => pkgs.length))
|
||||
readonly updateCount$: Observable<number> = this.patch
|
||||
.watch$('ui', 'auto-check-updates')
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() =>
|
||||
this.marketplaceService.getUpdates$().pipe(
|
||||
map(arr => {
|
||||
return arr.reduce(
|
||||
(acc, marketplace) => acc + marketplace.pkgs.length,
|
||||
0,
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ export class SnekDirective {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.setDbValue({
|
||||
pointer: '/gaming/snake/high-score',
|
||||
value: data.highScore,
|
||||
})
|
||||
await this.embassyApi.setDbValue(
|
||||
['gaming', 'snake', 'high-score'],
|
||||
data.highScore,
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ title }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ng-container>
|
||||
<div class="center text-center">
|
||||
<div class="card">
|
||||
<h4>This service was installed from:</h4>
|
||||
<p class="courier-new color-success-shade">{{ packageMarketplace }}</p>
|
||||
<h4>But you are currently connected to:</h4>
|
||||
<p class="courier-new color-primary-shade">{{ currentMarketplace }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Switch to {{ packageMarketplace }} in</p>
|
||||
<ion-button
|
||||
color="success"
|
||||
routerLink="embassy/marketplaces"
|
||||
(click)="dismiss()"
|
||||
>Marketplace Settings</ion-button
|
||||
>
|
||||
<p>Or you can</p>
|
||||
<ion-button
|
||||
[routerLink]="['marketplace/', pkgId]"
|
||||
click="dismiss()"
|
||||
(click)="dismiss()"
|
||||
>Continue to {{ currentMarketplace }}</ion-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ActionMarketplaceComponent } from './action-marketplace.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActionMarketplaceComponent],
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild([])],
|
||||
exports: [ActionMarketplaceComponent],
|
||||
})
|
||||
export class ActionMarketplaceComponentModule {}
|
||||
@@ -1,17 +0,0 @@
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(53, 56, 62, 0.768);
|
||||
border-radius: 7px;
|
||||
padding: 27px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'action-marketplace',
|
||||
templateUrl: './action-marketplace.component.html',
|
||||
styleUrls: ['./action-marketplace.component.scss'],
|
||||
})
|
||||
export class ActionMarketplaceComponent {
|
||||
@Input() title!: string
|
||||
@Input() packageMarketplace!: string
|
||||
@Input() currentMarketplace!: string
|
||||
@Input() pkgId!: string
|
||||
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
|
||||
dismiss() {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,39 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ConfigService } from '../../services/config.service'
|
||||
import { ApiService } from '../../services/api/embassy-api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'os-update',
|
||||
templateUrl: './os-update.page.html',
|
||||
styleUrls: ['./os-update.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OSUpdatePage {
|
||||
@Input() releaseNotes!: { [version: string]: string }
|
||||
|
||||
versions: { version: string; notes: string }[] = []
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly config: ConfigService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly eosService: EOSService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.versions = Object.keys(this.releaseNotes)
|
||||
const releaseNotes = this.eosService.eos?.['release-notes']!
|
||||
|
||||
this.versions = Object.keys(releaseNotes)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(version => {
|
||||
return {
|
||||
version,
|
||||
notes: this.releaseNotes[version],
|
||||
notes: releaseNotes[version],
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -45,9 +49,7 @@ export class OSUpdatePage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.updateServer({
|
||||
'marketplace-url': this.config.marketplace.url,
|
||||
})
|
||||
await this.embassyApi.updateServer()
|
||||
this.dismiss()
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
|
||||
@@ -161,10 +161,7 @@ export class AppActionsPage {
|
||||
try {
|
||||
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
||||
this.embassyApi
|
||||
.setDbValue({
|
||||
pointer: `/ack-instructions/${this.pkgId}`,
|
||||
value: false,
|
||||
})
|
||||
.setDbValue(['ack-instructions', this.pkgId], false)
|
||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -8,15 +8,10 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="loading else loaded">
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
</ng-container>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-template #loaded>
|
||||
<ng-container *ngIf="pkgs$ | async as pkgs; else loading">
|
||||
<app-list-empty
|
||||
*ngIf="empty; else list"
|
||||
*ngIf="!pkgs.length; else list"
|
||||
class="ion-text-center ion-padding"
|
||||
></app-list-empty>
|
||||
|
||||
@@ -36,5 +31,10 @@
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<!-- loading -->
|
||||
<ng-template #loading>
|
||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,46 +1,27 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { filter, takeUntil, tap } from 'rxjs/operators'
|
||||
import { DestroyService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { filter, map, pairwise, startWith } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'app-list',
|
||||
templateUrl: './app-list.page.html',
|
||||
styleUrls: ['./app-list.page.scss'],
|
||||
providers: [DestroyService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListPage {
|
||||
loading = true
|
||||
pkgs: readonly PackageDataEntry[] = []
|
||||
readonly pkgs$ = this.patch.watch$('package-data').pipe(
|
||||
map(pkgs => Object.values(pkgs)),
|
||||
startWith([]),
|
||||
pairwise(),
|
||||
filter(([prev, next]) => {
|
||||
const length = next.length
|
||||
return !length || prev.length !== length
|
||||
}),
|
||||
map(([_, pkgs]) => {
|
||||
return pkgs.sort((a, b) => (b.manifest.title > a.manifest.title ? -1 : 1))
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
get empty(): boolean {
|
||||
return !this.pkgs.length
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.patch
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
filter(pkgs => Object.keys(pkgs).length !== this.pkgs.length),
|
||||
tap(pkgs => {
|
||||
this.loading = false
|
||||
this.pkgs = Object.values(pkgs).sort((a, b) =>
|
||||
b.manifest.title > a.manifest.title ? -1 : 1,
|
||||
)
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||
import { ActionMarketplaceComponentModule } from 'src/app/modals/action-marketplace/action-marketplace.component.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -54,7 +53,6 @@ const routes: Routes = [
|
||||
EmverPipesModule,
|
||||
LaunchablePipeModule,
|
||||
UiPipeModule,
|
||||
ActionMarketplaceComponentModule,
|
||||
],
|
||||
})
|
||||
export class AppShowPageModule {}
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
[dependencies]="dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu
|
||||
[buttons]="pkg | toButtons: (currentMarketplace$ | async): (altMarketplaceData$ | async)"
|
||||
></app-show-menu>
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
import { filter, tap } from 'rxjs/operators'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
@@ -49,16 +47,10 @@ export class AppShowPage {
|
||||
),
|
||||
)
|
||||
|
||||
readonly currentMarketplace$ = this.marketplaceService.getMarketplace()
|
||||
|
||||
readonly altMarketplaceData$ = this.marketplaceService.getAltMarketplaceData()
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
isInstalled({ state }: PackageDataEntry): boolean {
|
||||
|
||||
@@ -2,17 +2,14 @@ import { Inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||
import { getUrlHostname, MarkdownComponent } from '@start9labs/shared'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
UIMarketplaceData,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ModalService } from 'src/app/services/modal.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { from, map, Observable } from 'rxjs'
|
||||
import { Marketplace } from '@start9labs/marketplace'
|
||||
import { ActionMarketplaceComponent } from 'src/app/modals/action-marketplace/action-marketplace.component'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
|
||||
export interface Button {
|
||||
@@ -39,11 +36,7 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
transform(
|
||||
pkg: PackageDataEntry,
|
||||
currentMarketplace: Marketplace | null,
|
||||
altMarketplaces: UIMarketplaceData | null | undefined,
|
||||
): Button[] {
|
||||
transform(pkg: PackageDataEntry): Button[] {
|
||||
const pkgTitle = pkg.manifest.title
|
||||
|
||||
return [
|
||||
@@ -103,7 +96,7 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
icon: 'receipt-outline',
|
||||
},
|
||||
// view in marketplace
|
||||
this.viewInMarketplaceButton(pkg, currentMarketplace, altMarketplaces),
|
||||
this.viewInMarketplaceButton(pkg),
|
||||
// donate
|
||||
{
|
||||
action: () => this.donate(pkg),
|
||||
@@ -116,10 +109,7 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
|
||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||
this.apiService
|
||||
.setDbValue({
|
||||
pointer: `/ack-instructions/${pkg.manifest.id}`,
|
||||
value: true,
|
||||
})
|
||||
.setDbValue(['ack-instructions', pkg.manifest.id], true)
|
||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
@@ -135,51 +125,27 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private viewInMarketplaceButton(
|
||||
pkg: PackageDataEntry,
|
||||
currentMarketplace: Marketplace | null,
|
||||
altMarketplaces: UIMarketplaceData | null | undefined,
|
||||
): Button {
|
||||
const pkgMarketplaceUrl = pkg.installed?.['marketplace-url']
|
||||
// default button if package marketplace and current marketplace are the same
|
||||
private viewInMarketplaceButton(pkg: PackageDataEntry): Button {
|
||||
const url = pkg.installed?.['marketplace-url']
|
||||
const queryParams = url ? { url } : {}
|
||||
|
||||
let button: Button = {
|
||||
title: 'Marketplace',
|
||||
icon: 'storefront-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`]),
|
||||
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
|
||||
queryParams,
|
||||
}),
|
||||
disabled: false,
|
||||
description: 'View service in marketplace',
|
||||
}
|
||||
if (!pkgMarketplaceUrl) {
|
||||
|
||||
if (!url) {
|
||||
button.disabled = true
|
||||
button.description = 'This package was not installed from a marketplace.'
|
||||
button.action = () => {}
|
||||
} else if (
|
||||
pkgMarketplaceUrl &&
|
||||
currentMarketplace &&
|
||||
getUrlHostname(pkgMarketplaceUrl) !==
|
||||
getUrlHostname(currentMarketplace.url)
|
||||
) {
|
||||
// attempt to get name for pkg marketplace
|
||||
let pkgMarketplaceName = getUrlHostname(pkgMarketplaceUrl)
|
||||
if (altMarketplaces) {
|
||||
const pkgMarketplaces = Object.values(
|
||||
altMarketplaces['known-hosts'],
|
||||
).filter(m => getUrlHostname(m.url) === pkgMarketplaceName)
|
||||
if (pkgMarketplaces.length) {
|
||||
// if multiple of the same url exist, they will have the same name, so fine to grab first
|
||||
pkgMarketplaceName = pkgMarketplaces[0].name
|
||||
}
|
||||
}
|
||||
|
||||
button.action = () =>
|
||||
this.differentMarketplaceAction(
|
||||
pkgMarketplaceName,
|
||||
currentMarketplace.name,
|
||||
pkg.manifest.id,
|
||||
)
|
||||
button.description = 'Service was installed from a different marketplace'
|
||||
}
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
@@ -195,22 +161,4 @@ export class ToButtonsPipe implements PipeTransform {
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
private async differentMarketplaceAction(
|
||||
packageMarketplace: string,
|
||||
currentMarketplace: string,
|
||||
pkgId: string,
|
||||
) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: ActionMarketplaceComponent,
|
||||
componentProps: {
|
||||
title: 'Marketplace Conflict',
|
||||
packageMarketplace,
|
||||
currentMarketplace,
|
||||
pkgId,
|
||||
},
|
||||
cssClass: 'medium-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||
import { filter, map, startWith } from 'rxjs/operators'
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import { map, startWith } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@@ -27,7 +27,6 @@ export class ToHealthChecksPipe implements PipeTransform {
|
||||
const healthChecks$ = this.patch
|
||||
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main')
|
||||
.pipe(
|
||||
filter(obj => exists(obj)),
|
||||
map(main => {
|
||||
// Question: is this ok or do we have to use Object.keys
|
||||
// to maintain order and the keys initially present in pkg?
|
||||
|
||||
@@ -69,10 +69,7 @@ export class DevConfigPage {
|
||||
async save() {
|
||||
this.saving = true
|
||||
try {
|
||||
await this.api.setDbValue({
|
||||
pointer: `/dev/${this.projectId}/config`,
|
||||
value: this.code,
|
||||
})
|
||||
await this.api.setDbValue(['dev', this.projectId, 'config'], this.code)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -56,10 +56,10 @@ export class DevInstructionsPage {
|
||||
async save() {
|
||||
this.saving = true
|
||||
try {
|
||||
await this.api.setDbValue({
|
||||
pointer: `/dev/${this.projectId}/instructions`,
|
||||
value: this.code,
|
||||
})
|
||||
await this.api.setDbValue(
|
||||
['dev', this.projectId, 'instructions'],
|
||||
this.code,
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -148,7 +148,7 @@ export class DeveloperListPage {
|
||||
.replace(/warning:/g, '# Optional\n warning:')
|
||||
|
||||
const def = { name, config, instructions: SAMPLE_INSTUCTIONS }
|
||||
await this.api.setDbValue({ pointer: `/dev/${id}`, value: def })
|
||||
await this.api.setDbValue(['dev', id], def)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -184,7 +184,7 @@ export class DeveloperListPage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName })
|
||||
await this.api.setDbValue(['dev', id, 'name'], newName)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
@@ -201,7 +201,7 @@ export class DeveloperListPage {
|
||||
try {
|
||||
const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData))
|
||||
delete devDataToSave[id]
|
||||
await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave })
|
||||
await this.api.setDbValue(['dev'], devDataToSave)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -55,10 +55,10 @@ export class DeveloperMenuPage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({
|
||||
pointer: `/dev/${this.projectId}/basic-info`,
|
||||
value: basicInfo,
|
||||
})
|
||||
await this.api.setDbValue(
|
||||
['dev', this.projectId, 'basic-info'],
|
||||
basicInfo,
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Marketplace</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
@@ -28,14 +29,13 @@
|
||||
</ion-row>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col size="12">
|
||||
<ng-container *ngIf="pkgs$ | async as pkgs; else loading">
|
||||
<ng-container *ngIf="marketplace$ | async as marketplace; else loading">
|
||||
<ng-container *ngIf="localPkgs$ | async as localPkgs">
|
||||
<marketplace-categories
|
||||
*ngIf="categories$ | async as categories"
|
||||
[categories]="categories"
|
||||
[categories]="marketplace.categories"
|
||||
[category]="category"
|
||||
[updatesAvailable]="
|
||||
(pkgs | filterPackages: '':'updates':localPkgs).length
|
||||
(marketplace.pkgs | filterPackages: '':'updates':localPkgs).length
|
||||
"
|
||||
(categoryChange)="onCategoryChange($event)"
|
||||
></marketplace-categories>
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="divider"></div>
|
||||
|
||||
<ion-grid
|
||||
*ngIf="pkgs | filterPackages: query:category:localPkgs as filtered"
|
||||
*ngIf="marketplace.pkgs | filterPackages: query:category:localPkgs as filtered"
|
||||
>
|
||||
<div
|
||||
*ngIf="!filtered.length && category === 'updates'"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { getUrlHostname } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
@@ -12,25 +12,45 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceListPage {
|
||||
private readonly pkgs$ = this.marketplaceService.getPackages$()
|
||||
|
||||
private readonly categories$ = this.marketplaceService
|
||||
.getMarketplaceInfo$()
|
||||
.pipe(
|
||||
map(({ categories }) => {
|
||||
const set = new Set<string>()
|
||||
if (categories.includes('featured')) set.add('featured')
|
||||
set.add('updates')
|
||||
categories.forEach(c => set.add(c))
|
||||
set.add('all')
|
||||
return set
|
||||
}),
|
||||
)
|
||||
|
||||
readonly marketplace$ = combineLatest([this.pkgs$, this.categories$]).pipe(
|
||||
map(arr => {
|
||||
return { pkgs: arr[0], categories: arr[1] }
|
||||
}),
|
||||
)
|
||||
|
||||
readonly localPkgs$ = this.patch.watch$('package-data')
|
||||
readonly categories$ = this.marketplaceService.getCategories()
|
||||
readonly pkgs$ = this.marketplaceService.getPackages()
|
||||
readonly details$ = this.marketplaceService.getMarketplace().pipe(
|
||||
map(d => {
|
||||
|
||||
readonly details$ = this.marketplaceService.getUiMarketplace$().pipe(
|
||||
map(({ url, name }) => {
|
||||
let color: string
|
||||
let description: string
|
||||
switch (getUrlHostname(d.url)) {
|
||||
case 'registry.start9.com':
|
||||
switch (url) {
|
||||
case 'https://registry.start9.com/':
|
||||
color = 'success'
|
||||
description =
|
||||
'Services in this marketplace are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service in this marketplace, one of our dedicated support staff will be happy to assist you.'
|
||||
break
|
||||
case 'beta-registry-0-3.start9labs.com':
|
||||
case 'https://beta-registry-0-3.start9labs.com/':
|
||||
color = 'primary'
|
||||
description =
|
||||
'Services in this marketplace are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
|
||||
break
|
||||
case 'community.start9labs.com':
|
||||
case 'https://community.start9labs.com/':
|
||||
color = 'tertiary'
|
||||
description =
|
||||
'Services in this marketplace are packaged and maintained by members of the Start9 community. <b>Install at your own risk</b>. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.'
|
||||
@@ -43,7 +63,8 @@ export class MarketplaceListPage {
|
||||
}
|
||||
|
||||
return {
|
||||
...d,
|
||||
name,
|
||||
url,
|
||||
color,
|
||||
description,
|
||||
}
|
||||
@@ -52,7 +73,8 @@ export class MarketplaceListPage {
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
) {}
|
||||
|
||||
category = 'featured'
|
||||
|
||||
@@ -31,6 +31,9 @@ import { firstValueFrom } from 'rxjs'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowControlsComponent {
|
||||
@Input()
|
||||
url?: string
|
||||
|
||||
@Input()
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
@@ -58,22 +61,81 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
|
||||
async tryInstall() {
|
||||
const currentMarketplace = await firstValueFrom(
|
||||
this.marketplaceService.getUiMarketplace$(),
|
||||
)
|
||||
const url = this.url || currentMarketplace.url
|
||||
|
||||
if (!this.localPkg) {
|
||||
this.alertInstall()
|
||||
this.alertInstall(url)
|
||||
} else {
|
||||
const originalUrl = this.localPkg.installed?.['marketplace-url']
|
||||
|
||||
if (url !== originalUrl) {
|
||||
const proceed = await this.presentAlertDifferentMarketplace(
|
||||
url,
|
||||
originalUrl,
|
||||
)
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
if (
|
||||
this.emver.compare(this.localVersion, this.pkg.manifest.version) !==
|
||||
0 &&
|
||||
hasCurrentDeps(this.localPkg)
|
||||
) {
|
||||
this.dryInstall()
|
||||
this.dryInstall(url)
|
||||
} else {
|
||||
this.install()
|
||||
this.install(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async dryInstall() {
|
||||
private async presentAlertDifferentMarketplace(
|
||||
url: string,
|
||||
originalUrl: string | null | undefined,
|
||||
): Promise<boolean> {
|
||||
const marketplaces = await firstValueFrom(
|
||||
this.patch.watch$('ui', 'marketplace'),
|
||||
)
|
||||
|
||||
const name = marketplaces['known-hosts'][url] || url
|
||||
|
||||
let originalName: string | undefined
|
||||
if (originalUrl) {
|
||||
originalName = marketplaces['known-hosts'][originalUrl] || originalUrl
|
||||
}
|
||||
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: `This service was originally ${
|
||||
originalName ? 'installed from ' + originalName : 'side loaded'
|
||||
}, but you are currently connected to ${name}. To install from ${name} anyway, click "Continue".`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
resolve(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
resolve(true)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
cssClass: 'alert-warning-message',
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
})
|
||||
}
|
||||
|
||||
private async dryInstall(url: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Checking dependent services...',
|
||||
})
|
||||
@@ -88,12 +150,12 @@ export class MarketplaceShowControlsComponent {
|
||||
})
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(loader)
|
||||
this.install(url, loader)
|
||||
} else {
|
||||
await loader.dismiss()
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install()
|
||||
this.install(url)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -101,10 +163,10 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async alertInstall() {
|
||||
private async alertInstall(url: string) {
|
||||
const installAlert = this.pkg.manifest.alerts.install
|
||||
|
||||
if (!installAlert) return this.install()
|
||||
if (!installAlert) return this.install(url)
|
||||
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Alert',
|
||||
@@ -117,7 +179,7 @@ export class MarketplaceShowControlsComponent {
|
||||
{
|
||||
text: 'Install',
|
||||
handler: () => {
|
||||
this.install()
|
||||
this.install(url)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
@@ -126,7 +188,7 @@ export class MarketplaceShowControlsComponent {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async install(loader?: HTMLIonLoadingElement) {
|
||||
private async install(url: string, loader?: HTMLIonLoadingElement) {
|
||||
const message = 'Beginning Install...'
|
||||
if (loader) {
|
||||
loader.message = message
|
||||
@@ -138,12 +200,7 @@ export class MarketplaceShowControlsComponent {
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
try {
|
||||
await firstValueFrom(
|
||||
this.marketplaceService.installPackage({
|
||||
id,
|
||||
'version-spec': `=${version}`,
|
||||
}),
|
||||
)
|
||||
await this.marketplaceService.installPackage(id, version, url)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<ng-container *ngIf="!(pkg | empty)">
|
||||
<marketplace-package [pkg]="pkg"></marketplace-package>
|
||||
<marketplace-show-controls
|
||||
[url]="url"
|
||||
[pkg]="pkg"
|
||||
[localPkg]="localPkg$ | async"
|
||||
></marketplace-show-controls>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ErrorToastService, getPkgId } from '@start9labs/shared'
|
||||
import {
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
} from '@start9labs/marketplace'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
||||
import { catchError, filter, shareReplay, switchMap } from 'rxjs/operators'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { filter, shareReplay, switchMap } from 'rxjs/operators'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
@@ -18,6 +15,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
})
|
||||
export class MarketplaceShowPage {
|
||||
private readonly pkgId = getPkgId(this.route)
|
||||
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
|
||||
|
||||
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
||||
|
||||
@@ -25,26 +23,15 @@ export class MarketplaceShowPage {
|
||||
.watch$('package-data', this.pkgId)
|
||||
.pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true }))
|
||||
|
||||
readonly pkg$: Observable<MarketplacePkg | null> = this.loadVersion$.pipe(
|
||||
readonly pkg$ = this.loadVersion$.pipe(
|
||||
switchMap(version =>
|
||||
this.marketplaceService.getPackage(this.pkgId, version),
|
||||
this.marketplaceService.getPackage(this.pkgId, version, this.url),
|
||||
),
|
||||
// TODO: Better fallback
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
|
||||
return of({} as MarketplacePkg)
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
) {}
|
||||
|
||||
getIcon(icon: string): string {
|
||||
return `data:image/png;base64,${icon}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,34 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group>
|
||||
<ion-item-divider>Saved Marketplaces</ion-item-divider>
|
||||
<ion-item-group *ngIf="marketplace$ | async as m">
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
Connect to a standard marketplaces or an alternative marketplace.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Standard Marketplaces</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let s of m.standard"
|
||||
detail="false"
|
||||
[button]="s.url !== m.selected"
|
||||
(click)="s.url === m.selected ? '' : presentAction(s)"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="s.url === m.selected"
|
||||
slot="end"
|
||||
size="large"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ s.name }}</h2>
|
||||
<p>{{ s.url }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Alt Marketplaces</ion-item-divider>
|
||||
<ion-item button detail="false" (click)="presentModalAdd()">
|
||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
||||
<ion-label>
|
||||
@@ -20,22 +46,21 @@
|
||||
</ion-item>
|
||||
|
||||
<ion-item
|
||||
*ngFor="let mp of marketplaces"
|
||||
*ngFor="let a of m.alt"
|
||||
detail="false"
|
||||
[button]="mp.id !== selectedId"
|
||||
(click)="presentAction(mp.id)"
|
||||
[button]="a.url !== m.selected"
|
||||
(click)="a.url === m.selected ? '' : presentAction(a, true)"
|
||||
>
|
||||
<div *ngIf="mp.id !== selectedId" slot="start" class="padding"></div>
|
||||
<ion-icon
|
||||
*ngIf="mp.id === selectedId"
|
||||
slot="start"
|
||||
*ngIf="a.url === m.selected"
|
||||
slot="end"
|
||||
size="large"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ mp.name }}</h2>
|
||||
<p>{{ mp.url }}</p>
|
||||
<h2>{{ a.name }}</h2>
|
||||
<p>{{ a.url }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import {
|
||||
ActionSheetController,
|
||||
AlertController,
|
||||
@@ -6,47 +6,44 @@ import {
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ActionSheetButton } from '@ionic/core'
|
||||
import {
|
||||
DestroyService,
|
||||
ErrorToastService,
|
||||
getUrlHostname,
|
||||
} from '@start9labs/shared'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { v4 } from 'uuid'
|
||||
import {
|
||||
DataModel,
|
||||
UIMarketplaceData,
|
||||
} from '../../../services/patch-db/data-model'
|
||||
import { ConfigService } from '../../../services/config.service'
|
||||
import { DataModel } from '../../../services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
first,
|
||||
takeUntil,
|
||||
} from 'rxjs/operators'
|
||||
import { getServerInfo } from '../../../util/get-server-info'
|
||||
import { getMarketplace } from '../../../util/get-marketplace'
|
||||
|
||||
type Marketplaces = {
|
||||
id: string | null
|
||||
name: string
|
||||
url: string
|
||||
}[]
|
||||
import { map } from 'rxjs/operators'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplaces',
|
||||
templateUrl: 'marketplaces.page.html',
|
||||
styleUrls: ['marketplaces.page.scss'],
|
||||
providers: [DestroyService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplacesPage {
|
||||
selectedId: string | null = null
|
||||
marketplaces: Marketplaces = []
|
||||
marketplace$ = this.patch.watch$('ui', 'marketplace').pipe(
|
||||
map(m => {
|
||||
const selected = m['selected-url']
|
||||
const hosts = Object.entries(m['known-hosts'])
|
||||
|
||||
const standard = hosts
|
||||
.map(([url, name]) => {
|
||||
return { url, name }
|
||||
})
|
||||
.slice(0, 2) // 0 and 1 will always be prod and community
|
||||
|
||||
const alt = hosts
|
||||
.map(([url, name]) => {
|
||||
return { url, name }
|
||||
})
|
||||
.slice(2) // 2 and beyond will always be alts
|
||||
|
||||
return { selected, standard, alt }
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
@@ -56,55 +53,28 @@ export class MarketplacesPage {
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly destroy$: DestroyService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.patch
|
||||
.watch$('ui', 'marketplace')
|
||||
.pipe(distinctUntilChanged(), takeUntil(this.destroy$))
|
||||
.subscribe((mp: UIMarketplaceData) => {
|
||||
let marketplaces: Marketplaces = [
|
||||
{
|
||||
id: null,
|
||||
name: this.config.marketplace.name,
|
||||
url: this.config.marketplace.url,
|
||||
},
|
||||
]
|
||||
this.selectedId = mp['selected-id']
|
||||
const alts = Object.entries(mp['known-hosts']).map(([k, v]) => {
|
||||
return {
|
||||
id: k,
|
||||
name: v.name,
|
||||
url: v.url,
|
||||
}
|
||||
})
|
||||
marketplaces = marketplaces.concat(alts)
|
||||
this.marketplaces = marketplaces
|
||||
})
|
||||
}
|
||||
|
||||
async presentModalAdd() {
|
||||
const marketplaceSpec = getMarketplaceValueSpec()
|
||||
const { name, spec } = getMarketplaceValueSpec()
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: marketplaceSpec.name,
|
||||
spec: marketplaceSpec.spec,
|
||||
title: name,
|
||||
spec,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save for Later',
|
||||
handler: (value: { url: string }) => {
|
||||
this.save(value.url)
|
||||
this.saveOnly(new URL(value.url))
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Save and Connect',
|
||||
handler: (value: { url: string }) => {
|
||||
this.saveAndConnect(value.url)
|
||||
this.saveAndConnect(new URL(value.url))
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
@@ -116,32 +86,31 @@ export class MarketplacesPage {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAction(id: string | null) {
|
||||
// no need to view actions if is selected marketplace
|
||||
const marketplace = await getMarketplace(this.patch)
|
||||
if (id === marketplace['selected-id']) return
|
||||
|
||||
async presentAction(
|
||||
{ url, name }: { url: string; name: string },
|
||||
canDelete = false,
|
||||
) {
|
||||
const buttons: ActionSheetButton[] = [
|
||||
{
|
||||
text: 'Connect',
|
||||
handler: () => {
|
||||
this.connect(id)
|
||||
this.connect(url)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (id) {
|
||||
if (canDelete) {
|
||||
buttons.unshift({
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
handler: () => {
|
||||
this.presentAlertDelete(id)
|
||||
this.presentAlertDelete(url, name)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const action = await this.actionCtrl.create({
|
||||
header: this.marketplaces.find(mp => mp.id === id)?.name,
|
||||
header: name,
|
||||
mode: 'ios',
|
||||
buttons,
|
||||
})
|
||||
@@ -149,55 +118,7 @@ export class MarketplacesPage {
|
||||
await action.present()
|
||||
}
|
||||
|
||||
private async connect(id: string | null): Promise<void> {
|
||||
const marketplace = await getMarketplace(this.patch)
|
||||
|
||||
const url = id
|
||||
? marketplace['known-hosts'][id].url
|
||||
: this.config.marketplace.url
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Validating Marketplace...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const { id } = await getServerInfo(this.patch)
|
||||
await this.marketplaceService.getMarketplaceData({ 'server-id': id }, url)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
loader.message = 'Changing Marketplace...'
|
||||
|
||||
const value: UIMarketplaceData = {
|
||||
...marketplace,
|
||||
'selected-id': id,
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value })
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
}
|
||||
|
||||
loader.message = 'Syncing store...'
|
||||
|
||||
this.marketplaceService
|
||||
.getPackages()
|
||||
.pipe(
|
||||
first(),
|
||||
finalize(() => loader.dismiss()),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private async presentAlertDelete(id: string) {
|
||||
const name = this.marketplaces.find(m => m.id === id)?.name
|
||||
|
||||
private async presentAlertDelete(url: string, name: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to delete ${name}?`,
|
||||
@@ -208,7 +129,7 @@ export class MarketplacesPage {
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
handler: () => this.delete(id),
|
||||
handler: () => this.delete(url),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
@@ -217,125 +138,104 @@ export class MarketplacesPage {
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async delete(id: string): Promise<void> {
|
||||
const data = await getMarketplace(this.patch)
|
||||
const marketplace: UIMarketplaceData = JSON.parse(JSON.stringify(data))
|
||||
private async connect(
|
||||
url: string,
|
||||
loader?: HTMLIonLoadingElement,
|
||||
): Promise<void> {
|
||||
const message = 'Changing Marketplace...'
|
||||
if (!loader) {
|
||||
loader = await this.loadingCtrl.create({ message })
|
||||
await loader.present()
|
||||
} else {
|
||||
loader.message = message
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.setDbValue(['marketplace', 'selected-url'], url)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveOnly(url: URL): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create()
|
||||
|
||||
try {
|
||||
await this.validateAndSave(url, loader)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAndConnect(url: URL): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create()
|
||||
|
||||
try {
|
||||
await this.validateAndSave(url, loader)
|
||||
await this.connect(url.toString(), loader)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async validateAndSave(
|
||||
urlObj: URL,
|
||||
loader: HTMLIonLoadingElement,
|
||||
): Promise<void> {
|
||||
const url = urlObj.toString()
|
||||
// Error on duplicates
|
||||
const hosts = await firstValueFrom(
|
||||
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
|
||||
)
|
||||
const currentUrls = Object.keys(hosts)
|
||||
if (currentUrls.includes(url)) throw new Error('marketplace already added')
|
||||
|
||||
// Validate
|
||||
loader.message = 'Validating marketplace...'
|
||||
await loader.present()
|
||||
|
||||
const name = await this.marketplaceService.validateMarketplace(url)
|
||||
|
||||
// Save
|
||||
loader.message = 'Saving...'
|
||||
|
||||
await this.api.setDbValue(['marketplace', 'known-hosts', url], name)
|
||||
}
|
||||
|
||||
private async delete(url: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Deleting...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
const hosts = await firstValueFrom(
|
||||
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
|
||||
)
|
||||
|
||||
const filtered = Object.keys(hosts)
|
||||
.filter(key => key !== url)
|
||||
.reduce((prev, curr) => {
|
||||
const name = hosts[curr]
|
||||
return {
|
||||
...prev,
|
||||
[curr]: name,
|
||||
}
|
||||
}, {})
|
||||
|
||||
try {
|
||||
delete marketplace['known-hosts'][id]
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
await this.api.setDbValue(['marketplace', 'known-hosts'], filtered)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async save(url: string): Promise<void> {
|
||||
const data = await getMarketplace(this.patch)
|
||||
const marketplace: UIMarketplaceData = data
|
||||
? JSON.parse(JSON.stringify(data))
|
||||
: {
|
||||
'selected-id': null,
|
||||
'known-hosts': {},
|
||||
}
|
||||
|
||||
// no-op on duplicates
|
||||
const currentUrls = this.marketplaces.map(mp => getUrlHostname(mp.url))
|
||||
if (currentUrls.includes(getUrlHostname(url))) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Validating Marketplace...',
|
||||
})
|
||||
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const id = v4()
|
||||
const { id: serverId } = await getServerInfo(this.patch)
|
||||
const { name } = await this.marketplaceService.getMarketplaceData(
|
||||
{ 'server-id': serverId },
|
||||
url,
|
||||
)
|
||||
marketplace['known-hosts'][id] = { name, url }
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
loader.message = 'Saving...'
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAndConnect(url: string): Promise<void> {
|
||||
const data = await getMarketplace(this.patch)
|
||||
const marketplace: UIMarketplaceData = data
|
||||
? JSON.parse(JSON.stringify(data))
|
||||
: {
|
||||
'selected-id': null,
|
||||
'known-hosts': {},
|
||||
}
|
||||
|
||||
// no-op on duplicates
|
||||
const currentUrls = this.marketplaces.map(mp => getUrlHostname(mp.url))
|
||||
if (currentUrls.includes(getUrlHostname(url))) {
|
||||
this.errToast.present({ message: 'Marketplace already added' })
|
||||
return
|
||||
}
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Validating Marketplace...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const id = v4()
|
||||
const { id: serverId } = await getServerInfo(this.patch)
|
||||
const { name } = await this.marketplaceService.getMarketplaceData(
|
||||
{ 'server-id': serverId },
|
||||
url,
|
||||
)
|
||||
marketplace['known-hosts'][id] = { name, url }
|
||||
marketplace['selected-id'] = id
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
loader.message = 'Saving...'
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
loader.dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
loader.message = 'Syncing marketplace data...'
|
||||
|
||||
this.marketplaceService
|
||||
.getPackages()
|
||||
.pipe(
|
||||
first(),
|
||||
finalize(() => loader.dismiss()),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
function getMarketplaceValueSpec(): ValueSpecObject {
|
||||
|
||||
@@ -73,7 +73,7 @@ export class PreferencesPage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/${key}`, value })
|
||||
await this.api.setDbValue([key], value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
|
||||
@@ -48,9 +48,6 @@ export class ServerShowPage {
|
||||
|
||||
async updateEos(): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
releaseNotes: this.eosService.eos?.['release-notes'],
|
||||
},
|
||||
component: OSUpdatePage,
|
||||
})
|
||||
modal.present()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Dump, Revision } from 'patch-db-client'
|
||||
import { MarketplaceData, MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { MarketplaceInfo, MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import {
|
||||
@@ -242,7 +242,7 @@ export module RR {
|
||||
// marketplace
|
||||
|
||||
export type GetMarketplaceDataReq = { 'server-id': string }
|
||||
export type GetMarketplaceDataRes = MarketplaceData
|
||||
export type GetMarketplaceDataRes = MarketplaceInfo
|
||||
|
||||
export type GetMarketplaceEOSReq = {
|
||||
'server-id': string
|
||||
|
||||
@@ -18,7 +18,10 @@ export abstract class ApiService {
|
||||
|
||||
// db
|
||||
|
||||
abstract setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes>
|
||||
abstract setDbValue(
|
||||
pathArr: Array<string | number>,
|
||||
value: any,
|
||||
): Promise<RR.SetDBValueRes>
|
||||
|
||||
// auth
|
||||
|
||||
@@ -64,7 +67,7 @@ export abstract class ApiService {
|
||||
params: RR.GetPackageMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes>
|
||||
|
||||
abstract updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes>
|
||||
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
||||
|
||||
abstract restartServer(
|
||||
params: RR.RestartServerReq,
|
||||
|
||||
@@ -18,10 +18,12 @@ import { Observable } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
import { PatchDB, Update } from 'patch-db-client'
|
||||
import { PatchDB, pathFromArray, Update } from 'patch-db-client'
|
||||
|
||||
@Injectable()
|
||||
export class LiveApiService extends ApiService {
|
||||
readonly eosMarketplaceUrl = 'https://registry.start9.com/'
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly http: HttpService,
|
||||
@@ -52,7 +54,12 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// db
|
||||
|
||||
async setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||
async setDbValue(
|
||||
pathArr: Array<string | number>,
|
||||
value: any,
|
||||
): Promise<RR.SetDBValueRes> {
|
||||
const pointer = pathFromArray(pathArr)
|
||||
const params: RR.SetDBValueReq = { pointer, value }
|
||||
return this.rpcRequest({ method: 'db.put.ui', params })
|
||||
}
|
||||
|
||||
@@ -127,13 +134,11 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.metrics', params })
|
||||
}
|
||||
|
||||
async updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
|
||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||
const params = {
|
||||
'marketplace-url': url || this.eosMarketplaceUrl,
|
||||
}
|
||||
return this.rpcRequest({ method: 'server.update', params })
|
||||
// const res = await this.updateServer(params)
|
||||
// if (res.response === 'no-updates') {
|
||||
// throw new Error('Could not find a newer version of embassyOS')
|
||||
// }
|
||||
// return res
|
||||
}
|
||||
|
||||
async restartServer(
|
||||
@@ -160,12 +165,12 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async marketplaceProxy<T>(path: string, qp: {}, url: string): Promise<T> {
|
||||
async marketplaceProxy<T>(path: string, qp: {}, baseUrl: string): Promise<T> {
|
||||
Object.assign(qp, { arch: this.config.targetArch })
|
||||
const fullURL = `${url}${path}?${new URLSearchParams(qp).toString()}`
|
||||
const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}`
|
||||
return this.rpcRequest({
|
||||
method: 'marketplace.get',
|
||||
params: { url: fullURL },
|
||||
params: { url: fullUrl },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,7 +180,7 @@ export class LiveApiService extends ApiService {
|
||||
return this.marketplaceProxy(
|
||||
'/eos/v0/latest',
|
||||
params,
|
||||
this.config.marketplace.url,
|
||||
this.eosMarketplaceUrl,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor, Log } from '@start9labs/shared'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client'
|
||||
import {
|
||||
PatchOp,
|
||||
Update,
|
||||
Operation,
|
||||
RemoveOperation,
|
||||
pathFromArray,
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyErrorType,
|
||||
@@ -15,11 +21,22 @@ import { CifsBackupTarget, RR } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../../assets/markdown/md-sample.md'
|
||||
import { BehaviorSubject, interval, map, Observable } from 'rxjs'
|
||||
import {
|
||||
EMPTY,
|
||||
iif,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
|
||||
const PROGRESS: InstallProgress = {
|
||||
size: 120,
|
||||
@@ -33,28 +50,35 @@ const PROGRESS: InstallProgress = {
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockWsSource$ = new BehaviorSubject<Update<DataModel>>({
|
||||
id: 1,
|
||||
value: mockPatchData,
|
||||
})
|
||||
readonly mockWsSource$ = new ReplaySubject<Update<DataModel>>()
|
||||
private readonly revertTime = 2000
|
||||
sequence = 0
|
||||
|
||||
constructor(
|
||||
private readonly bootstrapper: LocalStorageBootstrap,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly auth: AuthService,
|
||||
) {
|
||||
super()
|
||||
this.auth.isVerified$.subscribe(verified => {
|
||||
if (!verified) {
|
||||
this.patchStream$.next([])
|
||||
this.mockWsSource$.next({
|
||||
id: 1,
|
||||
value: mockPatchData,
|
||||
})
|
||||
this.sequence = 0
|
||||
}
|
||||
})
|
||||
this.auth.isVerified$
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.sequence = 0
|
||||
this.patchStream$.next([])
|
||||
}),
|
||||
switchMap(verified =>
|
||||
iif(
|
||||
() => verified,
|
||||
timer(2000).pipe(
|
||||
tap(() => {
|
||||
this.connectionService.websocketConnected$.next(true)
|
||||
}),
|
||||
),
|
||||
EMPTY,
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
async getStatic(url: string): Promise<string> {
|
||||
@@ -69,7 +93,12 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// db
|
||||
|
||||
async setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
|
||||
async setDbValue(
|
||||
pathArr: Array<string | number>,
|
||||
value: any,
|
||||
): Promise<RR.SetDBValueRes> {
|
||||
const pointer = pathFromArray(pathArr)
|
||||
const params: RR.SetDBValueReq = { pointer, value }
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
@@ -198,7 +227,7 @@ export class MockApiService extends ApiService {
|
||||
return Mock.getAppMetrics()
|
||||
}
|
||||
|
||||
async updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
|
||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress = {
|
||||
size: 10000,
|
||||
@@ -254,10 +283,10 @@ export class MockApiService extends ApiService {
|
||||
return {
|
||||
name: 'Dark69',
|
||||
categories: [
|
||||
'featured',
|
||||
'bitcoin',
|
||||
'lightning',
|
||||
'data',
|
||||
'featured',
|
||||
'messaging',
|
||||
'social',
|
||||
'alt coin',
|
||||
|
||||
@@ -15,12 +15,11 @@ export const mockPatchData: DataModel = {
|
||||
'pkg-order': [],
|
||||
'ack-welcome': '1.0.0',
|
||||
marketplace: {
|
||||
'selected-id': '1234',
|
||||
'selected-url': 'https://registry.start9.com/',
|
||||
'known-hosts': {
|
||||
'1234': {
|
||||
name: 'Dark9',
|
||||
url: 'https://test-marketplace.com',
|
||||
},
|
||||
'https://registry.start9.com/': 'Start9 Marketplace',
|
||||
'https://community-registry.start9.com/': 'Community Marketplace',
|
||||
'https://dark9-marketplace.com/': 'Dark9',
|
||||
},
|
||||
},
|
||||
dev: {},
|
||||
|
||||
@@ -11,7 +11,7 @@ const {
|
||||
targetArch,
|
||||
gitHash,
|
||||
useMocks,
|
||||
ui: { api, mocks, marketplace },
|
||||
ui: { api, mocks },
|
||||
} = require('../../../../../config.json') as WorkspaceConfig
|
||||
|
||||
@Injectable({
|
||||
@@ -25,7 +25,6 @@ export class ConfigService {
|
||||
targetArch = targetArch
|
||||
gitHash = gitHash
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
isConsulate = (window as any)['platform'] === 'ios'
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver, ErrorToastService } from '@start9labs/shared'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import {
|
||||
MarketplacePkg,
|
||||
AbstractMarketplaceService,
|
||||
Marketplace,
|
||||
FilterPackagesPipe,
|
||||
MarketplaceData,
|
||||
MarketplaceInfo,
|
||||
} from '@start9labs/marketplace'
|
||||
import { from, Observable, of, Subject } from 'rxjs'
|
||||
import { combineLatest, from, Observable, of } from 'rxjs'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import {
|
||||
DataModel,
|
||||
ServerInfo,
|
||||
UIMarketplaceData,
|
||||
Manifest,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
shareReplay,
|
||||
startWith,
|
||||
@@ -28,275 +23,321 @@ import {
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { getServerInfo } from '../util/get-server-info'
|
||||
|
||||
type MarketplaceURL = string
|
||||
interface MarketplaceData {
|
||||
info: MarketplaceInfo | null
|
||||
packages: MarketplacePkg[]
|
||||
}
|
||||
type MasterCache = Map<MarketplaceURL, MarketplaceData>
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceService extends AbstractMarketplaceService {
|
||||
private readonly notes = new Map<string, Record<string, string>>()
|
||||
private readonly hasPackages$ = new Subject<boolean>()
|
||||
export class MarketplaceService implements AbstractMarketplaceService {
|
||||
private readonly cache: MasterCache = new Map()
|
||||
|
||||
private readonly uiMarketplaceData$ = this.patch
|
||||
.watch$('ui', 'marketplace')
|
||||
.pipe(
|
||||
private readonly uiMarketplace$: Observable<{ url: string; name: string }> =
|
||||
this.patch.watch$('ui', 'marketplace').pipe(
|
||||
distinctUntilChanged(
|
||||
(prev, curr) => prev['selected-id'] === curr['selected-id'],
|
||||
(prev, curr) => prev['selected-url'] === curr['selected-url'],
|
||||
),
|
||||
map(data => {
|
||||
const url = data['selected-url']
|
||||
return {
|
||||
url,
|
||||
name: data['known-hosts'][url],
|
||||
}
|
||||
}),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly marketplace$ = this.uiMarketplaceData$.pipe(
|
||||
map(data => this.toMarketplace(data)),
|
||||
)
|
||||
|
||||
private readonly serverInfo$: Observable<ServerInfo> = this.patch
|
||||
.watch$('server-info')
|
||||
.pipe(take(1), shareReplay())
|
||||
|
||||
private readonly registryData$: Observable<MarketplaceData> =
|
||||
this.uiMarketplaceData$.pipe(
|
||||
switchMap(data =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(({ id }) =>
|
||||
from(
|
||||
this.getMarketplaceData(
|
||||
{ 'server-id': id },
|
||||
this.toMarketplace(data).url,
|
||||
),
|
||||
).pipe(tap(({ name }) => this.updateName(data, name))),
|
||||
),
|
||||
private readonly marketplaceData$: Observable<MarketplaceData> =
|
||||
this.uiMarketplace$.pipe(
|
||||
switchMap(({ url, name }) =>
|
||||
from(this.loadMarketplace(url)).pipe(
|
||||
tap(data => {
|
||||
this.updateName(url, name, data.info!.name)
|
||||
}),
|
||||
),
|
||||
),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly categories$: Observable<Set<string>> =
|
||||
this.registryData$.pipe(
|
||||
map(
|
||||
({ categories }) =>
|
||||
new Set(['featured', 'updates', ...categories, 'all']),
|
||||
),
|
||||
)
|
||||
private readonly marketplaceInfo$: Observable<MarketplaceInfo> =
|
||||
this.marketplaceData$.pipe(map(data => data.info!))
|
||||
|
||||
private readonly pkgs$: Observable<MarketplacePkg[]> = this.marketplace$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(info =>
|
||||
from(
|
||||
this.getMarketplacePkgs(
|
||||
{ page: 1, 'per-page': 100 },
|
||||
url,
|
||||
info['eos-version-compat'],
|
||||
),
|
||||
).pipe(tap(() => this.hasPackages$.next(true))),
|
||||
),
|
||||
),
|
||||
),
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
private readonly marketplacePkgs$: Observable<MarketplacePkg[]> =
|
||||
this.marketplaceData$.pipe(map(data => data.packages))
|
||||
|
||||
return of([])
|
||||
private readonly updates$: Observable<
|
||||
{ url: string; pkgs: MarketplacePkg[] }[]
|
||||
> = this.patch.watch$('package-data').pipe(
|
||||
take(1), // check once per app instance
|
||||
map(localPkgs => {
|
||||
return Object.values(localPkgs)
|
||||
.filter(localPkg => localPkg.state === PackageState.Installed)
|
||||
.reduce((localPkgMap, pkg) => {
|
||||
const url = pkg.installed!['marketplace-url'] || '' // side-laoded services will not have marketplace-url
|
||||
const cached = this.cache
|
||||
.get(url)
|
||||
?.packages.find(p => p.manifest.id === pkg.manifest.id)
|
||||
if (url && !cached) {
|
||||
const arr = localPkgMap.get(url) || []
|
||||
localPkgMap.set(url, arr.concat(pkg.manifest))
|
||||
}
|
||||
return localPkgMap
|
||||
}, new Map<string, Manifest[]>())
|
||||
}),
|
||||
switchMap(localPkgMap => {
|
||||
const urls = Array.from(localPkgMap.keys())
|
||||
const requests = urls.map(url => {
|
||||
const ids = localPkgMap.get(url)?.map(({ id }) => {
|
||||
return { id, version: '*' }
|
||||
})
|
||||
return from(this.loadPackages({ ids }, url)).pipe(
|
||||
map(pkgs => {
|
||||
const manifests = localPkgMap.get(url)!
|
||||
const filtered = pkgs.filter(pkg => {
|
||||
const localVersion = manifests.find(
|
||||
m => m.id === pkg.manifest.id,
|
||||
)?.version
|
||||
return (
|
||||
localVersion &&
|
||||
this.emver.compare(pkg.manifest.version, localVersion) === 1
|
||||
)
|
||||
})
|
||||
return { url, pkgs: filtered }
|
||||
}),
|
||||
startWith({ url, pkgs: [] }), // needed for combineLatest to emit right away
|
||||
)
|
||||
})
|
||||
return combineLatest(requests)
|
||||
}),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
private readonly updates$: Observable<MarketplacePkg[]> =
|
||||
this.hasPackages$.pipe(
|
||||
switchMap(() =>
|
||||
this.patch.watch$('package-data').pipe(
|
||||
switchMap(localPkgs =>
|
||||
this.pkgs$.pipe(
|
||||
map(pkgs => {
|
||||
return this.filterPkgsPipe.transform(
|
||||
pkgs,
|
||||
'',
|
||||
'updates',
|
||||
localPkgs,
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly emver: Emver,
|
||||
private readonly filterPkgsPipe: FilterPackagesPipe,
|
||||
) {
|
||||
super()
|
||||
) {}
|
||||
|
||||
getUiMarketplace$(): Observable<{ url: string; name: string }> {
|
||||
return this.uiMarketplace$
|
||||
}
|
||||
|
||||
getMarketplace(): Observable<Marketplace> {
|
||||
return this.marketplace$
|
||||
getMarketplaceInfo$(): Observable<MarketplaceInfo> {
|
||||
return this.marketplaceInfo$
|
||||
}
|
||||
|
||||
getAltMarketplaceData() {
|
||||
return this.uiMarketplaceData$
|
||||
getPackages$(): Observable<MarketplacePkg[]> {
|
||||
return this.marketplacePkgs$
|
||||
}
|
||||
|
||||
getCategories(): Observable<Set<string>> {
|
||||
return this.categories$
|
||||
}
|
||||
|
||||
getPackages(): Observable<MarketplacePkg[]> {
|
||||
return this.pkgs$
|
||||
}
|
||||
|
||||
getPackage(id: string, version: string): Observable<MarketplacePkg | null> {
|
||||
const params = { ids: [{ id, version }] }
|
||||
const fallback$ = this.marketplace$.pipe(
|
||||
switchMap(({ url }) =>
|
||||
this.serverInfo$.pipe(
|
||||
switchMap(info =>
|
||||
from(
|
||||
this.getMarketplacePkgs(params, url, info['eos-version-compat']),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
map(pkgs => this.findPackage(pkgs, id, version)),
|
||||
startWith(null),
|
||||
)
|
||||
|
||||
return this.getPackages().pipe(
|
||||
map(pkgs => this.findPackage(pkgs, id, version)),
|
||||
switchMap(pkg => (pkg ? of(pkg) : fallback$)),
|
||||
filter((pkg): pkg is MarketplacePkg | null => {
|
||||
if (pkg === undefined) {
|
||||
throw new Error(`No results for ${id}${version ? ' ' + version : ''}`)
|
||||
getPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
url?: string,
|
||||
): Observable<MarketplacePkg | undefined> {
|
||||
return this.uiMarketplace$.pipe(
|
||||
switchMap(m => {
|
||||
url = url || m.url
|
||||
if (this.cache.has(url)) {
|
||||
const pkg = this.getPkgFromCache(id, version, url)
|
||||
if (pkg) return of(pkg)
|
||||
}
|
||||
|
||||
return true
|
||||
if (version === '*') {
|
||||
return from(this.loadPackage(id, url))
|
||||
} else {
|
||||
return from(this.fetchPackage(id, version, url))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
getUpdates(): Observable<MarketplacePkg[]> {
|
||||
getUpdates$(): Observable<{ url: string; pkgs: MarketplacePkg[] }[]> {
|
||||
return this.updates$
|
||||
}
|
||||
|
||||
getReleaseNotes(id: string): Observable<Record<string, string>> {
|
||||
if (this.notes.has(id)) {
|
||||
return of(this.notes.get(id) || {})
|
||||
async installPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
url: string,
|
||||
): Promise<void> {
|
||||
const params: RR.InstallPackageReq = {
|
||||
id,
|
||||
'version-spec': `=${version}`,
|
||||
'marketplace-url': url,
|
||||
}
|
||||
|
||||
return this.marketplace$.pipe(
|
||||
switchMap(({ url }) => this.loadReleaseNotes(id, url)),
|
||||
tap(response => this.notes.set(id, response)),
|
||||
catchError(e => {
|
||||
this.errToast.present(e)
|
||||
await this.api.installPackage(params)
|
||||
}
|
||||
|
||||
return of({})
|
||||
async validateMarketplace(url: string): Promise<string> {
|
||||
await this.loadInfo(url)
|
||||
return this.cache.get(url)!.info!.name
|
||||
}
|
||||
|
||||
fetchReleaseNotes(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>> {
|
||||
return this.uiMarketplace$.pipe(
|
||||
switchMap(m => {
|
||||
return from(
|
||||
this.api.marketplaceProxy<Record<string, string>>(
|
||||
`/package/v0/release-notes/${id}`,
|
||||
{},
|
||||
url || m.url,
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
installPackage(
|
||||
req: Omit<RR.InstallPackageReq, 'marketplace-url'>,
|
||||
): Observable<unknown> {
|
||||
return this.getMarketplace().pipe(
|
||||
take(1),
|
||||
switchMap(({ url }) =>
|
||||
from(
|
||||
this.api.installPackage({
|
||||
...req,
|
||||
'marketplace-url': url,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
getPackageMarkdown(type: string, pkgId: string): Observable<string> {
|
||||
return this.getMarketplace().pipe(
|
||||
switchMap(({ url }) =>
|
||||
from(
|
||||
fetchPackageMarkdown(
|
||||
id: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
): Observable<string> {
|
||||
return this.uiMarketplace$.pipe(
|
||||
switchMap(m => {
|
||||
return from(
|
||||
this.api.marketplaceProxy<string>(
|
||||
`/package/v0/${type}/${pkgId}`,
|
||||
`/package/v0/${type}/${id}`,
|
||||
{},
|
||||
url,
|
||||
url || m.url,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async getMarketplaceData(
|
||||
params: RR.GetMarketplaceDataReq,
|
||||
url: string,
|
||||
): Promise<RR.GetMarketplaceDataRes> {
|
||||
return this.api.marketplaceProxy('/package/v0/info', params, url)
|
||||
private async loadMarketplace(url: string): Promise<MarketplaceData> {
|
||||
const cachedInfo = this.cache.get(url)?.info
|
||||
const [info, packages] = await Promise.all([
|
||||
cachedInfo || this.loadInfo(url),
|
||||
this.loadPackages({}, url),
|
||||
])
|
||||
return { info, packages }
|
||||
}
|
||||
|
||||
async getMarketplacePkgs(
|
||||
params: Omit<RR.GetMarketplacePackagesReq, 'eos-version-compat'>,
|
||||
url: string,
|
||||
eosVersionCompat: string,
|
||||
): Promise<RR.GetMarketplacePackagesRes> {
|
||||
let clonedParams = { ...params }
|
||||
if (params.query) delete params.category
|
||||
if (clonedParams.ids) clonedParams.ids = JSON.stringify(clonedParams.ids)
|
||||
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...clonedParams,
|
||||
'eos-version-compat': eosVersionCompat,
|
||||
}
|
||||
|
||||
return this.api.marketplaceProxy('/package/v0/index', qp, url)
|
||||
private async loadInfo(url: string): Promise<MarketplaceInfo> {
|
||||
const info = await this.fetchInfo(url)
|
||||
this.updateCache(url, info)
|
||||
return info
|
||||
}
|
||||
|
||||
private loadReleaseNotes(
|
||||
private async loadPackage(
|
||||
id: string,
|
||||
url: string,
|
||||
): Observable<Record<string, string>> {
|
||||
return from(
|
||||
this.api.marketplaceProxy<Record<string, string>>(
|
||||
`/package/v0/release-notes/${id}`,
|
||||
{},
|
||||
url,
|
||||
),
|
||||
): Promise<MarketplacePkg | undefined> {
|
||||
const pkgs = await this.loadPackages({ ids: [{ id, version: '*' }] }, url)
|
||||
return pkgs[0]
|
||||
}
|
||||
|
||||
private async loadPackages(
|
||||
params: Omit<
|
||||
RR.GetMarketplacePackagesReq,
|
||||
'eos-version-compat' | 'page' | 'per-page'
|
||||
>,
|
||||
url: string,
|
||||
): Promise<MarketplacePkg[]> {
|
||||
const pkgs = await this.fetchPackages(params, url)
|
||||
this.updateCache(url, undefined, pkgs)
|
||||
return pkgs
|
||||
}
|
||||
|
||||
private async fetchInfo(url: string): Promise<RR.GetMarketplaceDataRes> {
|
||||
const { id } = await getServerInfo(this.patch)
|
||||
|
||||
const params: RR.GetMarketplaceDataReq = {
|
||||
'server-id': id,
|
||||
}
|
||||
|
||||
return this.api.marketplaceProxy<RR.GetMarketplaceDataRes>(
|
||||
'/package/v0/info',
|
||||
params,
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
||||
private updateName(
|
||||
uiMarketplaceData: UIMarketplaceData | undefined,
|
||||
name: string,
|
||||
) {
|
||||
if (!uiMarketplaceData?.['selected-id']) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedId = uiMarketplaceData['selected-id']
|
||||
const knownHosts = uiMarketplaceData['known-hosts']
|
||||
|
||||
if (knownHosts[selectedId].name !== name) {
|
||||
this.api.setDbValue({
|
||||
pointer: `/marketplace/known-hosts/${selectedId}/name`,
|
||||
value: name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private toMarketplace(marketplace: UIMarketplaceData): Marketplace {
|
||||
return marketplace['selected-id']
|
||||
? marketplace['known-hosts'][marketplace['selected-id']]
|
||||
: this.config.marketplace
|
||||
}
|
||||
|
||||
private findPackage(
|
||||
pkgs: readonly MarketplacePkg[],
|
||||
private async fetchPackage(
|
||||
id: string,
|
||||
version: string,
|
||||
): MarketplacePkg | undefined {
|
||||
return pkgs.find(pkg => {
|
||||
const versionIsSame =
|
||||
version === '*' ||
|
||||
this.emver.compare(pkg.manifest.version, version) === 0
|
||||
url: string,
|
||||
): Promise<MarketplacePkg | undefined> {
|
||||
const pkgs = await this.fetchPackages({ ids: [{ id, version }] }, url)
|
||||
return pkgs[0]
|
||||
}
|
||||
|
||||
return pkg.manifest.id === id && versionIsSame
|
||||
private async fetchPackages(
|
||||
params: Omit<
|
||||
RR.GetMarketplacePackagesReq,
|
||||
'eos-version-compat' | 'page' | 'per-page'
|
||||
>,
|
||||
url: string,
|
||||
): Promise<RR.GetMarketplacePackagesRes> {
|
||||
const qp: RR.GetMarketplacePackagesReq = {
|
||||
...params,
|
||||
'eos-version-compat': (await getServerInfo(this.patch))[
|
||||
'eos-version-compat'
|
||||
],
|
||||
page: 1,
|
||||
'per-page': 100,
|
||||
}
|
||||
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
|
||||
|
||||
return this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
|
||||
'/package/v0/index',
|
||||
qp,
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
||||
private async updateName(
|
||||
url: string,
|
||||
name: string,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
if (name !== newName) {
|
||||
this.api.setDbValue(['marketplace', 'known-hosts', url], newName)
|
||||
}
|
||||
}
|
||||
|
||||
private getPkgFromCache(
|
||||
id: string,
|
||||
version: string,
|
||||
url: string,
|
||||
): MarketplacePkg | undefined {
|
||||
return this.cache.get(url)?.packages.find(p => {
|
||||
const versionIsSame =
|
||||
version === '*' || this.emver.compare(p.manifest.version, version) === 0
|
||||
|
||||
return p.manifest.id === id && versionIsSame
|
||||
})
|
||||
}
|
||||
|
||||
private updateCache(
|
||||
url: string,
|
||||
info?: MarketplaceInfo,
|
||||
pkgs?: MarketplacePkg[],
|
||||
): void {
|
||||
const cache = this.cache.get(url)
|
||||
|
||||
let packages = cache?.packages || []
|
||||
if (pkgs) {
|
||||
const filtered = packages.filter(
|
||||
cachedPkg =>
|
||||
!pkgs.find(pkg => pkg.manifest.id === cachedPkg.manifest.id),
|
||||
)
|
||||
packages = filtered.concat(pkgs)
|
||||
}
|
||||
|
||||
this.cache.set(url, {
|
||||
info: info || cache?.info || null,
|
||||
packages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Inject, Injectable } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { Observable } from 'rxjs'
|
||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
||||
import { EOSService } from 'src/app/services/eos.service'
|
||||
@@ -21,7 +20,6 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
private readonly stream$ = this.connectionService.connected$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() => this.patch.watch$()),
|
||||
filter(obj => exists(obj) && !isEmptyObject(obj)),
|
||||
take(1),
|
||||
tap(({ ui }) => {
|
||||
// check for updates to EOS and services
|
||||
@@ -48,8 +46,8 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
private checkForUpdates(ui: UIData): void {
|
||||
if (ui['auto-check-updates'] !== false) {
|
||||
this.eosService.getEOS()
|
||||
this.marketplaceService.getPackages().pipe(take(1)).subscribe()
|
||||
this.marketplaceService.getCategories().pipe(take(1)).subscribe()
|
||||
this.marketplaceService.getMarketplaceInfo$().pipe(take(1)).subscribe()
|
||||
this.marketplaceService.getUpdates$().pipe(take(1)).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +62,7 @@ export class PatchDataService extends Observable<DataModel> {
|
||||
backdropDismiss: false,
|
||||
})
|
||||
modal.onWillDismiss().then(() => {
|
||||
this.embassyApi
|
||||
.setDbValue({ pointer: '/ack-welcome', value: this.config.version })
|
||||
.catch()
|
||||
this.embassyApi.setDbValue(['ack-welcome'], this.config.version).catch()
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
|
||||
@@ -25,12 +25,11 @@ export interface UIData {
|
||||
}
|
||||
|
||||
export interface UIMarketplaceData {
|
||||
'selected-id': string | null
|
||||
'selected-url': string
|
||||
'known-hosts': {
|
||||
[id: string]: {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
'https://registry.start9.com/': string
|
||||
'https://community-registry.start9.com/': string
|
||||
[url: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,10 +100,7 @@ export class ServerConfigService {
|
||||
|
||||
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
||||
'auto-check-updates': async (enabled: boolean) => {
|
||||
return this.embassyApi.setDbValue({
|
||||
pointer: '/auto-check-updates',
|
||||
value: enabled,
|
||||
})
|
||||
return this.embassyApi.setDbValue(['auto-check-updates'], enabled)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
UIMarketplaceData,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { firstValueFrom, map } from 'rxjs'
|
||||
|
||||
export function getMarketplace(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<UIMarketplaceData> {
|
||||
return firstValueFrom(patch.watch$('ui', 'marketplace'))
|
||||
}
|
||||
@@ -3,16 +3,16 @@ import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { filter, firstValueFrom } from 'rxjs'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
|
||||
export function getPackage(
|
||||
export async function getPackage(
|
||||
patch: PatchDB<DataModel>,
|
||||
id: string,
|
||||
): Promise<PackageDataEntry | undefined> {
|
||||
return firstValueFrom(patch.watch$('package-data', id))
|
||||
}
|
||||
|
||||
export function getAllPackages(
|
||||
export async function getAllPackages(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<Record<string, PackageDataEntry>> {
|
||||
return firstValueFrom(patch.watch$('package-data'))
|
||||
|
||||
@@ -2,6 +2,8 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
|
||||
export function getServerInfo(patch: PatchDB<DataModel>): Promise<ServerInfo> {
|
||||
export async function getServerInfo(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<ServerInfo> {
|
||||
return firstValueFrom(patch.watch$('server-info'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user