mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +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": {
|
"mocks": {
|
||||||
"maskAs": "tor",
|
"maskAs": "tor",
|
||||||
"skipStartupAlerts": true
|
"skipStartupAlerts": true
|
||||||
},
|
|
||||||
"marketplace": {
|
|
||||||
"url": "https://registry.start9.com/",
|
|
||||||
"name": "Start9 Marketplace"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gitHash": ""
|
"gitHash": ""
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": null,
|
"name": null,
|
||||||
"auto-check-updates": true,
|
"auto-check-updates": true,
|
||||||
"pkg-order": [],
|
"ack-welcome": "0.3.3",
|
||||||
"ack-welcome": "0.3.2.1",
|
|
||||||
"marketplace": {
|
"marketplace": {
|
||||||
"selected-id": null,
|
"selected-url": "https://registry.start9.com/",
|
||||||
"known-hosts": {}
|
"known-hosts": {
|
||||||
|
"https://registry.start9.com/": "Start9 Marketplace",
|
||||||
|
"https://community-registry.start9.com/": "Community Marketplace"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"dev": {},
|
"dev": {},
|
||||||
"gaming": {
|
"gaming": {
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
})
|
})
|
||||||
export class CategoriesComponent {
|
export class CategoriesComponent {
|
||||||
@Input()
|
@Input()
|
||||||
categories = new Set<string>()
|
categories!: Set<string>
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
category = ''
|
category!: string
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
updatesAvailable = 0
|
updatesAvailable!: number
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
readonly categoryChange = new EventEmitter<string>()
|
readonly categoryChange = new EventEmitter<string>()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class ReleaseNotesComponent {
|
|||||||
|
|
||||||
private selected: string | null = null
|
private selected: string | null = null
|
||||||
|
|
||||||
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
|
readonly notes$ = this.marketplaceService.fetchReleaseNotes(this.pkgId)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { AlertController, ModalController } from '@ionic/angular'
|
import { AlertController, ModalController } from '@ionic/angular'
|
||||||
import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared'
|
import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared'
|
||||||
|
|
||||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||||
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
import { MarketplacePkg } from '../../../types/marketplace-pkg'
|
||||||
|
|
||||||
@@ -58,9 +57,9 @@ export class AdditionalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async presentModalMd(title: string) {
|
async presentModalMd(title: string) {
|
||||||
const content = this.marketplaceService.getPackageMarkdown(
|
const content = this.marketplaceService.fetchPackageMarkdown(
|
||||||
title,
|
|
||||||
this.pkg.manifest.id,
|
this.pkg.manifest.id,
|
||||||
|
title,
|
||||||
)
|
)
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
<div class="text">
|
<div class="text">
|
||||||
<h1 class="title">{{ pkg.manifest.title }}</h1>
|
<h1 class="title">{{ pkg.manifest.title }}</h1>
|
||||||
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
|
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
|
||||||
<!-- @TODO remove conditional when registry code deployed. published-at will be required -->
|
<p class="published">
|
||||||
<p *ngIf="pkg['published-at']" class="published">
|
|
||||||
Released: {{ pkg['published-at'] | date: 'medium' }}
|
Released: {{ pkg['published-at'] | date: 'medium' }}
|
||||||
</p>
|
</p>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
import { NgModule, Pipe, PipeTransform } from '@angular/core'
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
|
|
||||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||||
import { MarketplaceManifest } from '../types/marketplace-manifest'
|
import { MarketplaceManifest } from '../types/marketplace-manifest'
|
||||||
import { Emver } from '@start9labs/shared'
|
import { Emver } from '@start9labs/shared'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'filterPackages',
|
name: 'filterPackages',
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export * from './pipes/filter-packages.pipe'
|
|||||||
export * from './services/marketplace.service'
|
export * from './services/marketplace.service'
|
||||||
|
|
||||||
export * from './types/dependency'
|
export * from './types/dependency'
|
||||||
export * from './types/marketplace'
|
export * from './types/marketplace-info'
|
||||||
export * from './types/marketplace-data'
|
|
||||||
export * from './types/marketplace-manifest'
|
export * from './types/marketplace-manifest'
|
||||||
export * from './types/marketplace-pkg'
|
export * from './types/marketplace-pkg'
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
|
import { MarketplaceInfo } from '../types/marketplace-info'
|
||||||
import { MarketplacePkg } from '../types/marketplace-pkg'
|
import { MarketplacePkg } from '../types/marketplace-pkg'
|
||||||
import { Marketplace } from '../types/marketplace'
|
|
||||||
|
|
||||||
export abstract class AbstractMarketplaceService {
|
export abstract class AbstractMarketplaceService {
|
||||||
abstract getMarketplace(): Observable<Marketplace>
|
abstract getMarketplaceInfo$(): Observable<MarketplaceInfo>
|
||||||
|
|
||||||
abstract getReleaseNotes(id: string): Observable<Record<string, string>>
|
abstract getPackages$(): Observable<MarketplacePkg[]>
|
||||||
|
|
||||||
abstract getCategories(): Observable<Set<string>>
|
|
||||||
|
|
||||||
abstract getPackages(): Observable<MarketplacePkg[]>
|
|
||||||
|
|
||||||
abstract getPackageMarkdown(type: string, pkgId: string): Observable<string>
|
|
||||||
|
|
||||||
abstract getPackage(
|
abstract getPackage(
|
||||||
id: string,
|
id: string,
|
||||||
version: 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 {
|
export interface MarketplaceInfo {
|
||||||
categories: string[]
|
|
||||||
name: string
|
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 {
|
try {
|
||||||
return this.emver.compare(first, second) as SemverResult
|
return this.emver.compare(first, second) as SemverResult
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`emver comparison failed`, e, first, second)
|
console.error(`emver comparison failed`, e, first, second)
|
||||||
return 'comparison-impossible'
|
return 'comparison-impossible'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export class HttpService {
|
|||||||
@Inject(DOCUMENT) private readonly document: Document,
|
@Inject(DOCUMENT) private readonly document: Document,
|
||||||
private readonly http: HttpClient,
|
private readonly http: HttpClient,
|
||||||
) {
|
) {
|
||||||
const { protocol, hostname, port } = this.document.location
|
this.fullUrl = this.document.location.origin
|
||||||
this.fullUrl = `${protocol}//${hostname}:${port}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async rpcRequest<T>(
|
async rpcRequest<T>(
|
||||||
|
|||||||
@@ -12,9 +12,5 @@ export type WorkspaceConfig = {
|
|||||||
maskAs: 'tor' | 'lan'
|
maskAs: 'tor' | 'lan'
|
||||||
skipStartupAlerts: boolean
|
skipStartupAlerts: boolean
|
||||||
}
|
}
|
||||||
marketplace: {
|
|
||||||
url: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||||
import { EOSService } from '../../services/eos.service'
|
import { EOSService } from '../../services/eos.service'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { Observable } from 'rxjs'
|
import { iif, Observable } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
import { filter, map, switchMap } from 'rxjs/operators'
|
||||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -47,9 +47,21 @@ export class MenuComponent {
|
|||||||
|
|
||||||
readonly showEOSUpdate$ = this.eosService.showUpdate$
|
readonly showEOSUpdate$ = this.eosService.showUpdate$
|
||||||
|
|
||||||
readonly updateCount$: Observable<number> = this.marketplaceService
|
readonly updateCount$: Observable<number> = this.patch
|
||||||
.getUpdates()
|
.watch$('ui', 'auto-check-updates')
|
||||||
.pipe(map(pkgs => pkgs.length))
|
.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$
|
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ export class SnekDirective {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.setDbValue({
|
await this.embassyApi.setDbValue(
|
||||||
pointer: '/gaming/snake/high-score',
|
['gaming', 'snake', 'high-score'],
|
||||||
value: data.highScore,
|
data.highScore,
|
||||||
})
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} 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 { LoadingController, ModalController } from '@ionic/angular'
|
||||||
import { ConfigService } from '../../services/config.service'
|
|
||||||
import { ApiService } from '../../services/api/embassy-api.service'
|
import { ApiService } from '../../services/api/embassy-api.service'
|
||||||
import { ErrorToastService } from '@start9labs/shared'
|
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({
|
@Component({
|
||||||
selector: 'os-update',
|
selector: 'os-update',
|
||||||
templateUrl: './os-update.page.html',
|
templateUrl: './os-update.page.html',
|
||||||
styleUrls: ['./os-update.page.scss'],
|
styleUrls: ['./os-update.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class OSUpdatePage {
|
export class OSUpdatePage {
|
||||||
@Input() releaseNotes!: { [version: string]: string }
|
|
||||||
|
|
||||||
versions: { version: string; notes: string }[] = []
|
versions: { version: string; notes: string }[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
private readonly config: ConfigService,
|
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
|
private readonly eosService: EOSService,
|
||||||
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.versions = Object.keys(this.releaseNotes)
|
const releaseNotes = this.eosService.eos?.['release-notes']!
|
||||||
|
|
||||||
|
this.versions = Object.keys(releaseNotes)
|
||||||
.sort()
|
.sort()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map(version => {
|
.map(version => {
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
notes: this.releaseNotes[version],
|
notes: releaseNotes[version],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,9 +49,7 @@ export class OSUpdatePage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.embassyApi.updateServer({
|
await this.embassyApi.updateServer()
|
||||||
'marketplace-url': this.config.marketplace.url,
|
|
||||||
})
|
|
||||||
this.dismiss()
|
this.dismiss()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
|
|||||||
@@ -161,10 +161,7 @@ export class AppActionsPage {
|
|||||||
try {
|
try {
|
||||||
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
await this.embassyApi.uninstallPackage({ id: this.pkgId })
|
||||||
this.embassyApi
|
this.embassyApi
|
||||||
.setDbValue({
|
.setDbValue(['ack-instructions', this.pkgId], false)
|
||||||
pointer: `/ack-instructions/${this.pkgId}`,
|
|
||||||
value: false,
|
|
||||||
})
|
|
||||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||||
this.navCtrl.navigateRoot('/services')
|
this.navCtrl.navigateRoot('/services')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -8,15 +8,10 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<!-- loading -->
|
|
||||||
<ng-container *ngIf="loading else loaded">
|
|
||||||
<text-spinner text="Connecting to Embassy"></text-spinner>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- loaded -->
|
<!-- loaded -->
|
||||||
<ng-template #loaded>
|
<ng-container *ngIf="pkgs$ | async as pkgs; else loading">
|
||||||
<app-list-empty
|
<app-list-empty
|
||||||
*ngIf="empty; else list"
|
*ngIf="!pkgs.length; else list"
|
||||||
class="ion-text-center ion-padding"
|
class="ion-text-center ion-padding"
|
||||||
></app-list-empty>
|
></app-list-empty>
|
||||||
|
|
||||||
@@ -36,5 +31,10 @@
|
|||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- loading -->
|
||||||
|
<ng-template #loading>
|
||||||
|
<text-spinner text="Connecting to Embassy"></text-spinner>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,46 +1,27 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
DataModel,
|
import { filter, map, pairwise, startWith } from 'rxjs/operators'
|
||||||
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'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
templateUrl: './app-list.page.html',
|
templateUrl: './app-list.page.html',
|
||||||
styleUrls: ['./app-list.page.scss'],
|
styleUrls: ['./app-list.page.scss'],
|
||||||
providers: [DestroyService],
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppListPage {
|
export class AppListPage {
|
||||||
loading = true
|
readonly pkgs$ = this.patch.watch$('package-data').pipe(
|
||||||
pkgs: readonly PackageDataEntry[] = []
|
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(
|
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
|||||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||||
import { ActionMarketplaceComponentModule } from 'src/app/modals/action-marketplace/action-marketplace.component.module'
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -54,7 +53,6 @@ const routes: Routes = [
|
|||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
LaunchablePipeModule,
|
LaunchablePipeModule,
|
||||||
UiPipeModule,
|
UiPipeModule,
|
||||||
ActionMarketplaceComponentModule,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppShowPageModule {}
|
export class AppShowPageModule {}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@
|
|||||||
[dependencies]="dependencies"
|
[dependencies]="dependencies"
|
||||||
></app-show-dependencies>
|
></app-show-dependencies>
|
||||||
<!-- ** menu ** -->
|
<!-- ** menu ** -->
|
||||||
<app-show-menu
|
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||||
[buttons]="pkg | toButtons: (currentMarketplace$ | async): (altMarketplaceData$ | async)"
|
|
||||||
></app-show-menu>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
</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 { NavController } from '@ionic/angular'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
import { filter, tap } from 'rxjs/operators'
|
import { filter, tap } from 'rxjs/operators'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { getPkgId } from '@start9labs/shared'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
|
||||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
|
||||||
|
|
||||||
const STATES = [
|
const STATES = [
|
||||||
PackageState.Installing,
|
PackageState.Installing,
|
||||||
@@ -49,16 +47,10 @@ export class AppShowPage {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly currentMarketplace$ = this.marketplaceService.getMarketplace()
|
|
||||||
|
|
||||||
readonly altMarketplaceData$ = this.marketplaceService.getAltMarketplaceData()
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly navCtrl: NavController,
|
private readonly navCtrl: NavController,
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
@Inject(AbstractMarketplaceService)
|
|
||||||
private readonly marketplaceService: MarketplaceService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isInstalled({ state }: PackageDataEntry): boolean {
|
isInstalled({ state }: PackageDataEntry): boolean {
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ import { Inject, Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { DOCUMENT } from '@angular/common'
|
import { DOCUMENT } from '@angular/common'
|
||||||
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
import { AlertController, ModalController, NavController } from '@ionic/angular'
|
||||||
import { getUrlHostname, MarkdownComponent } from '@start9labs/shared'
|
import { MarkdownComponent } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
UIMarketplaceData,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { ModalService } from 'src/app/services/modal.service'
|
import { ModalService } from 'src/app/services/modal.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { from, map, Observable } from 'rxjs'
|
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'
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
|
||||||
export interface Button {
|
export interface Button {
|
||||||
@@ -39,11 +36,7 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
transform(
|
transform(pkg: PackageDataEntry): Button[] {
|
||||||
pkg: PackageDataEntry,
|
|
||||||
currentMarketplace: Marketplace | null,
|
|
||||||
altMarketplaces: UIMarketplaceData | null | undefined,
|
|
||||||
): Button[] {
|
|
||||||
const pkgTitle = pkg.manifest.title
|
const pkgTitle = pkg.manifest.title
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -103,7 +96,7 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
icon: 'receipt-outline',
|
icon: 'receipt-outline',
|
||||||
},
|
},
|
||||||
// view in marketplace
|
// view in marketplace
|
||||||
this.viewInMarketplaceButton(pkg, currentMarketplace, altMarketplaces),
|
this.viewInMarketplaceButton(pkg),
|
||||||
// donate
|
// donate
|
||||||
{
|
{
|
||||||
action: () => this.donate(pkg),
|
action: () => this.donate(pkg),
|
||||||
@@ -116,10 +109,7 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
|
|
||||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||||
this.apiService
|
this.apiService
|
||||||
.setDbValue({
|
.setDbValue(['ack-instructions', pkg.manifest.id], true)
|
||||||
pointer: `/ack-instructions/${pkg.manifest.id}`,
|
|
||||||
value: true,
|
|
||||||
})
|
|
||||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
@@ -135,51 +125,27 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewInMarketplaceButton(
|
private viewInMarketplaceButton(pkg: PackageDataEntry): Button {
|
||||||
pkg: PackageDataEntry,
|
const url = pkg.installed?.['marketplace-url']
|
||||||
currentMarketplace: Marketplace | null,
|
const queryParams = url ? { url } : {}
|
||||||
altMarketplaces: UIMarketplaceData | null | undefined,
|
|
||||||
): Button {
|
|
||||||
const pkgMarketplaceUrl = pkg.installed?.['marketplace-url']
|
|
||||||
// default button if package marketplace and current marketplace are the same
|
|
||||||
let button: Button = {
|
let button: Button = {
|
||||||
title: 'Marketplace',
|
title: 'Marketplace',
|
||||||
icon: 'storefront-outline',
|
icon: 'storefront-outline',
|
||||||
action: () =>
|
action: () =>
|
||||||
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`]),
|
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
|
||||||
|
queryParams,
|
||||||
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
description: 'View service in marketplace',
|
description: 'View service in marketplace',
|
||||||
}
|
}
|
||||||
if (!pkgMarketplaceUrl) {
|
|
||||||
|
if (!url) {
|
||||||
button.disabled = true
|
button.disabled = true
|
||||||
button.description = 'This package was not installed from a marketplace.'
|
button.description = 'This package was not installed from a marketplace.'
|
||||||
button.action = () => {}
|
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
|
return button
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,22 +161,4 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
await alert.present()
|
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,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
import { isEmptyObject } from '@start9labs/shared'
|
||||||
import { filter, map, startWith } from 'rxjs/operators'
|
import { map, startWith } from 'rxjs/operators'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ export class ToHealthChecksPipe implements PipeTransform {
|
|||||||
const healthChecks$ = this.patch
|
const healthChecks$ = this.patch
|
||||||
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main')
|
.watch$('package-data', pkg.manifest.id, 'installed', 'status', 'main')
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(obj => exists(obj)),
|
|
||||||
map(main => {
|
map(main => {
|
||||||
// Question: is this ok or do we have to use Object.keys
|
// Question: is this ok or do we have to use Object.keys
|
||||||
// to maintain order and the keys initially present in pkg?
|
// to maintain order and the keys initially present in pkg?
|
||||||
|
|||||||
@@ -69,10 +69,7 @@ export class DevConfigPage {
|
|||||||
async save() {
|
async save() {
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue({
|
await this.api.setDbValue(['dev', this.projectId, 'config'], this.code)
|
||||||
pointer: `/dev/${this.projectId}/config`,
|
|
||||||
value: this.code,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ export class DevInstructionsPage {
|
|||||||
async save() {
|
async save() {
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue({
|
await this.api.setDbValue(
|
||||||
pointer: `/dev/${this.projectId}/instructions`,
|
['dev', this.projectId, 'instructions'],
|
||||||
value: this.code,
|
this.code,
|
||||||
})
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export class DeveloperListPage {
|
|||||||
.replace(/warning:/g, '# Optional\n warning:')
|
.replace(/warning:/g, '# Optional\n warning:')
|
||||||
|
|
||||||
const def = { name, config, instructions: SAMPLE_INSTUCTIONS }
|
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) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -184,7 +184,7 @@ export class DeveloperListPage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue({ pointer: `/dev/${id}/name`, value: newName })
|
await this.api.setDbValue(['dev', id, 'name'], newName)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -201,7 +201,7 @@ export class DeveloperListPage {
|
|||||||
try {
|
try {
|
||||||
const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData))
|
const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData))
|
||||||
delete devDataToSave[id]
|
delete devDataToSave[id]
|
||||||
await this.api.setDbValue({ pointer: `/dev`, value: devDataToSave })
|
await this.api.setDbValue(['dev'], devDataToSave)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ export class DeveloperMenuPage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue({
|
await this.api.setDbValue(
|
||||||
pointer: `/dev/${this.projectId}/basic-info`,
|
['dev', this.projectId, 'basic-info'],
|
||||||
value: basicInfo,
|
basicInfo,
|
||||||
})
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
|
<ion-title>Marketplace</ion-title>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<badge-menu-button></badge-menu-button>
|
<badge-menu-button></badge-menu-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
@@ -28,14 +29,13 @@
|
|||||||
</ion-row>
|
</ion-row>
|
||||||
<ion-row class="ion-align-items-center">
|
<ion-row class="ion-align-items-center">
|
||||||
<ion-col size="12">
|
<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">
|
<ng-container *ngIf="localPkgs$ | async as localPkgs">
|
||||||
<marketplace-categories
|
<marketplace-categories
|
||||||
*ngIf="categories$ | async as categories"
|
[categories]="marketplace.categories"
|
||||||
[categories]="categories"
|
|
||||||
[category]="category"
|
[category]="category"
|
||||||
[updatesAvailable]="
|
[updatesAvailable]="
|
||||||
(pkgs | filterPackages: '':'updates':localPkgs).length
|
(marketplace.pkgs | filterPackages: '':'updates':localPkgs).length
|
||||||
"
|
"
|
||||||
(categoryChange)="onCategoryChange($event)"
|
(categoryChange)="onCategoryChange($event)"
|
||||||
></marketplace-categories>
|
></marketplace-categories>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<ion-grid
|
<ion-grid
|
||||||
*ngIf="pkgs | filterPackages: query:category:localPkgs as filtered"
|
*ngIf="marketplace.pkgs | filterPackages: query:category:localPkgs as filtered"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
*ngIf="!filtered.length && category === 'updates'"
|
*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 { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
import { getUrlHostname } from '@start9labs/shared'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
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'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,25 +12,45 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MarketplaceListPage {
|
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 localPkgs$ = this.patch.watch$('package-data')
|
||||||
readonly categories$ = this.marketplaceService.getCategories()
|
|
||||||
readonly pkgs$ = this.marketplaceService.getPackages()
|
readonly details$ = this.marketplaceService.getUiMarketplace$().pipe(
|
||||||
readonly details$ = this.marketplaceService.getMarketplace().pipe(
|
map(({ url, name }) => {
|
||||||
map(d => {
|
|
||||||
let color: string
|
let color: string
|
||||||
let description: string
|
let description: string
|
||||||
switch (getUrlHostname(d.url)) {
|
switch (url) {
|
||||||
case 'registry.start9.com':
|
case 'https://registry.start9.com/':
|
||||||
color = 'success'
|
color = 'success'
|
||||||
description =
|
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.'
|
'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
|
break
|
||||||
case 'beta-registry-0-3.start9labs.com':
|
case 'https://beta-registry-0-3.start9labs.com/':
|
||||||
color = 'primary'
|
color = 'primary'
|
||||||
description =
|
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.'
|
'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
|
break
|
||||||
case 'community.start9labs.com':
|
case 'https://community.start9labs.com/':
|
||||||
color = 'tertiary'
|
color = 'tertiary'
|
||||||
description =
|
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.'
|
'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 {
|
return {
|
||||||
...d,
|
name,
|
||||||
|
url,
|
||||||
color,
|
color,
|
||||||
description,
|
description,
|
||||||
}
|
}
|
||||||
@@ -52,7 +73,8 @@ export class MarketplaceListPage {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly marketplaceService: AbstractMarketplaceService,
|
@Inject(AbstractMarketplaceService)
|
||||||
|
private readonly marketplaceService: MarketplaceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
category = 'featured'
|
category = 'featured'
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ import { firstValueFrom } from 'rxjs'
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MarketplaceShowControlsComponent {
|
export class MarketplaceShowControlsComponent {
|
||||||
|
@Input()
|
||||||
|
url?: string
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
pkg!: MarketplacePkg
|
pkg!: MarketplacePkg
|
||||||
|
|
||||||
@@ -58,22 +61,81 @@ export class MarketplaceShowControlsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryInstall() {
|
async tryInstall() {
|
||||||
|
const currentMarketplace = await firstValueFrom(
|
||||||
|
this.marketplaceService.getUiMarketplace$(),
|
||||||
|
)
|
||||||
|
const url = this.url || currentMarketplace.url
|
||||||
|
|
||||||
if (!this.localPkg) {
|
if (!this.localPkg) {
|
||||||
this.alertInstall()
|
this.alertInstall(url)
|
||||||
} else {
|
} else {
|
||||||
|
const originalUrl = this.localPkg.installed?.['marketplace-url']
|
||||||
|
|
||||||
|
if (url !== originalUrl) {
|
||||||
|
const proceed = await this.presentAlertDifferentMarketplace(
|
||||||
|
url,
|
||||||
|
originalUrl,
|
||||||
|
)
|
||||||
|
if (!proceed) return
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.emver.compare(this.localVersion, this.pkg.manifest.version) !==
|
this.emver.compare(this.localVersion, this.pkg.manifest.version) !==
|
||||||
0 &&
|
0 &&
|
||||||
hasCurrentDeps(this.localPkg)
|
hasCurrentDeps(this.localPkg)
|
||||||
) {
|
) {
|
||||||
this.dryInstall()
|
this.dryInstall(url)
|
||||||
} else {
|
} 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({
|
const loader = await this.loadingCtrl.create({
|
||||||
message: 'Checking dependent services...',
|
message: 'Checking dependent services...',
|
||||||
})
|
})
|
||||||
@@ -88,12 +150,12 @@ export class MarketplaceShowControlsComponent {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (isEmptyObject(breakages)) {
|
if (isEmptyObject(breakages)) {
|
||||||
this.install(loader)
|
this.install(url, loader)
|
||||||
} else {
|
} else {
|
||||||
await loader.dismiss()
|
await loader.dismiss()
|
||||||
const proceed = await this.presentAlertBreakages(breakages)
|
const proceed = await this.presentAlertBreakages(breakages)
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
this.install()
|
this.install(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} 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
|
const installAlert = this.pkg.manifest.alerts.install
|
||||||
|
|
||||||
if (!installAlert) return this.install()
|
if (!installAlert) return this.install(url)
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Alert',
|
header: 'Alert',
|
||||||
@@ -117,7 +179,7 @@ export class MarketplaceShowControlsComponent {
|
|||||||
{
|
{
|
||||||
text: 'Install',
|
text: 'Install',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.install()
|
this.install(url)
|
||||||
},
|
},
|
||||||
cssClass: 'enter-click',
|
cssClass: 'enter-click',
|
||||||
},
|
},
|
||||||
@@ -126,7 +188,7 @@ export class MarketplaceShowControlsComponent {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async install(loader?: HTMLIonLoadingElement) {
|
private async install(url: string, loader?: HTMLIonLoadingElement) {
|
||||||
const message = 'Beginning Install...'
|
const message = 'Beginning Install...'
|
||||||
if (loader) {
|
if (loader) {
|
||||||
loader.message = message
|
loader.message = message
|
||||||
@@ -138,12 +200,7 @@ export class MarketplaceShowControlsComponent {
|
|||||||
const { id, version } = this.pkg.manifest
|
const { id, version } = this.pkg.manifest
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await firstValueFrom(
|
await this.marketplaceService.installPackage(id, version, url)
|
||||||
this.marketplaceService.installPackage({
|
|
||||||
id,
|
|
||||||
'version-spec': `=${version}`,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<ng-container *ngIf="!(pkg | empty)">
|
<ng-container *ngIf="!(pkg | empty)">
|
||||||
<marketplace-package [pkg]="pkg"></marketplace-package>
|
<marketplace-package [pkg]="pkg"></marketplace-package>
|
||||||
<marketplace-show-controls
|
<marketplace-show-controls
|
||||||
|
[url]="url"
|
||||||
[pkg]="pkg"
|
[pkg]="pkg"
|
||||||
[localPkg]="localPkg$ | async"
|
[localPkg]="localPkg$ | async"
|
||||||
></marketplace-show-controls>
|
></marketplace-show-controls>
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ErrorToastService, getPkgId } from '@start9labs/shared'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
import {
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
MarketplacePkg,
|
|
||||||
AbstractMarketplaceService,
|
|
||||||
} from '@start9labs/marketplace'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { catchError, filter, shareReplay, switchMap } from 'rxjs/operators'
|
import { filter, shareReplay, switchMap } from 'rxjs/operators'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -18,6 +15,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
})
|
})
|
||||||
export class MarketplaceShowPage {
|
export class MarketplaceShowPage {
|
||||||
private readonly pkgId = getPkgId(this.route)
|
private readonly pkgId = getPkgId(this.route)
|
||||||
|
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
|
||||||
|
|
||||||
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
readonly loadVersion$ = new BehaviorSubject<string>('*')
|
||||||
|
|
||||||
@@ -25,26 +23,15 @@ export class MarketplaceShowPage {
|
|||||||
.watch$('package-data', this.pkgId)
|
.watch$('package-data', this.pkgId)
|
||||||
.pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true }))
|
.pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true }))
|
||||||
|
|
||||||
readonly pkg$: Observable<MarketplacePkg | null> = this.loadVersion$.pipe(
|
readonly pkg$ = this.loadVersion$.pipe(
|
||||||
switchMap(version =>
|
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(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly errToast: ErrorToastService,
|
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly marketplaceService: AbstractMarketplaceService,
|
private readonly marketplaceService: AbstractMarketplaceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getIcon(icon: string): string {
|
|
||||||
return `data:image/png;base64,${icon}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,34 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding-top">
|
<ion-content class="ion-padding-top">
|
||||||
<ion-item-group>
|
<ion-item-group *ngIf="marketplace$ | async as m">
|
||||||
<ion-item-divider>Saved Marketplaces</ion-item-divider>
|
<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-item button detail="false" (click)="presentModalAdd()">
|
||||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
@@ -20,22 +46,21 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item
|
<ion-item
|
||||||
*ngFor="let mp of marketplaces"
|
*ngFor="let a of m.alt"
|
||||||
detail="false"
|
detail="false"
|
||||||
[button]="mp.id !== selectedId"
|
[button]="a.url !== m.selected"
|
||||||
(click)="presentAction(mp.id)"
|
(click)="a.url === m.selected ? '' : presentAction(a, true)"
|
||||||
>
|
>
|
||||||
<div *ngIf="mp.id !== selectedId" slot="start" class="padding"></div>
|
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="mp.id === selectedId"
|
*ngIf="a.url === m.selected"
|
||||||
slot="start"
|
slot="end"
|
||||||
size="large"
|
size="large"
|
||||||
name="checkmark"
|
name="checkmark"
|
||||||
color="success"
|
color="success"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ mp.name }}</h2>
|
<h2>{{ a.name }}</h2>
|
||||||
<p>{{ mp.url }}</p>
|
<p>{{ a.url }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Inject } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
ActionSheetController,
|
ActionSheetController,
|
||||||
AlertController,
|
AlertController,
|
||||||
@@ -6,47 +6,44 @@ import {
|
|||||||
ModalController,
|
ModalController,
|
||||||
} from '@ionic/angular'
|
} from '@ionic/angular'
|
||||||
import { ActionSheetButton } from '@ionic/core'
|
import { ActionSheetButton } from '@ionic/core'
|
||||||
import {
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
DestroyService,
|
|
||||||
ErrorToastService,
|
|
||||||
getUrlHostname,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { v4 } from 'uuid'
|
import { DataModel } from '../../../services/patch-db/data-model'
|
||||||
import {
|
|
||||||
DataModel,
|
|
||||||
UIMarketplaceData,
|
|
||||||
} from '../../../services/patch-db/data-model'
|
|
||||||
import { ConfigService } from '../../../services/config.service'
|
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import {
|
import { map } from 'rxjs/operators'
|
||||||
distinctUntilChanged,
|
import { firstValueFrom } from 'rxjs'
|
||||||
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
|
|
||||||
}[]
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'marketplaces',
|
selector: 'marketplaces',
|
||||||
templateUrl: 'marketplaces.page.html',
|
templateUrl: 'marketplaces.page.html',
|
||||||
styleUrls: ['marketplaces.page.scss'],
|
styleUrls: ['marketplaces.page.scss'],
|
||||||
providers: [DestroyService],
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MarketplacesPage {
|
export class MarketplacesPage {
|
||||||
selectedId: string | null = null
|
marketplace$ = this.patch.watch$('ui', 'marketplace').pipe(
|
||||||
marketplaces: Marketplaces = []
|
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(
|
constructor(
|
||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
@@ -56,55 +53,28 @@ export class MarketplacesPage {
|
|||||||
private readonly actionCtrl: ActionSheetController,
|
private readonly actionCtrl: ActionSheetController,
|
||||||
@Inject(AbstractMarketplaceService)
|
@Inject(AbstractMarketplaceService)
|
||||||
private readonly marketplaceService: MarketplaceService,
|
private readonly marketplaceService: MarketplaceService,
|
||||||
private readonly config: ConfigService,
|
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly destroy$: DestroyService,
|
|
||||||
private readonly alertCtrl: AlertController,
|
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() {
|
async presentModalAdd() {
|
||||||
const marketplaceSpec = getMarketplaceValueSpec()
|
const { name, spec } = getMarketplaceValueSpec()
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
component: GenericFormPage,
|
component: GenericFormPage,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: marketplaceSpec.name,
|
title: name,
|
||||||
spec: marketplaceSpec.spec,
|
spec,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Save for Later',
|
text: 'Save for Later',
|
||||||
handler: (value: { url: string }) => {
|
handler: (value: { url: string }) => {
|
||||||
this.save(value.url)
|
this.saveOnly(new URL(value.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Save and Connect',
|
text: 'Save and Connect',
|
||||||
handler: (value: { url: string }) => {
|
handler: (value: { url: string }) => {
|
||||||
this.saveAndConnect(value.url)
|
this.saveAndConnect(new URL(value.url))
|
||||||
},
|
},
|
||||||
isSubmit: true,
|
isSubmit: true,
|
||||||
},
|
},
|
||||||
@@ -116,32 +86,31 @@ export class MarketplacesPage {
|
|||||||
await modal.present()
|
await modal.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentAction(id: string | null) {
|
async presentAction(
|
||||||
// no need to view actions if is selected marketplace
|
{ url, name }: { url: string; name: string },
|
||||||
const marketplace = await getMarketplace(this.patch)
|
canDelete = false,
|
||||||
if (id === marketplace['selected-id']) return
|
) {
|
||||||
|
|
||||||
const buttons: ActionSheetButton[] = [
|
const buttons: ActionSheetButton[] = [
|
||||||
{
|
{
|
||||||
text: 'Connect',
|
text: 'Connect',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.connect(id)
|
this.connect(url)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (id) {
|
if (canDelete) {
|
||||||
buttons.unshift({
|
buttons.unshift({
|
||||||
text: 'Delete',
|
text: 'Delete',
|
||||||
role: 'destructive',
|
role: 'destructive',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.presentAlertDelete(id)
|
this.presentAlertDelete(url, name)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = await this.actionCtrl.create({
|
const action = await this.actionCtrl.create({
|
||||||
header: this.marketplaces.find(mp => mp.id === id)?.name,
|
header: name,
|
||||||
mode: 'ios',
|
mode: 'ios',
|
||||||
buttons,
|
buttons,
|
||||||
})
|
})
|
||||||
@@ -149,55 +118,7 @@ export class MarketplacesPage {
|
|||||||
await action.present()
|
await action.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connect(id: string | null): Promise<void> {
|
private async presentAlertDelete(url: string, name: string) {
|
||||||
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
|
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
message: `Are you sure you want to delete ${name}?`,
|
message: `Are you sure you want to delete ${name}?`,
|
||||||
@@ -208,7 +129,7 @@ export class MarketplacesPage {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Delete',
|
text: 'Delete',
|
||||||
handler: () => this.delete(id),
|
handler: () => this.delete(url),
|
||||||
cssClass: 'enter-click',
|
cssClass: 'enter-click',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -217,125 +138,104 @@ export class MarketplacesPage {
|
|||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async delete(id: string): Promise<void> {
|
private async connect(
|
||||||
const data = await getMarketplace(this.patch)
|
url: string,
|
||||||
const marketplace: UIMarketplaceData = JSON.parse(JSON.stringify(data))
|
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({
|
const loader = await this.loadingCtrl.create({
|
||||||
message: 'Deleting...',
|
message: 'Deleting...',
|
||||||
})
|
})
|
||||||
await loader.present()
|
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 {
|
try {
|
||||||
delete marketplace['known-hosts'][id]
|
await this.api.setDbValue(['marketplace', 'known-hosts'], filtered)
|
||||||
await this.api.setDbValue({ pointer: `/marketplace`, value: marketplace })
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
loader.dismiss()
|
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 {
|
function getMarketplaceValueSpec(): ValueSpecObject {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class PreferencesPage {
|
|||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.setDbValue({ pointer: `/${key}`, value })
|
await this.api.setDbValue([key], value)
|
||||||
} finally {
|
} finally {
|
||||||
loader.dismiss()
|
loader.dismiss()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ export class ServerShowPage {
|
|||||||
|
|
||||||
async updateEos(): Promise<void> {
|
async updateEos(): Promise<void> {
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
|
||||||
releaseNotes: this.eosService.eos?.['release-notes'],
|
|
||||||
},
|
|
||||||
component: OSUpdatePage,
|
component: OSUpdatePage,
|
||||||
})
|
})
|
||||||
modal.present()
|
modal.present()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Dump, Revision } from 'patch-db-client'
|
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 { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||||
import {
|
import {
|
||||||
@@ -242,7 +242,7 @@ export module RR {
|
|||||||
// marketplace
|
// marketplace
|
||||||
|
|
||||||
export type GetMarketplaceDataReq = { 'server-id': string }
|
export type GetMarketplaceDataReq = { 'server-id': string }
|
||||||
export type GetMarketplaceDataRes = MarketplaceData
|
export type GetMarketplaceDataRes = MarketplaceInfo
|
||||||
|
|
||||||
export type GetMarketplaceEOSReq = {
|
export type GetMarketplaceEOSReq = {
|
||||||
'server-id': string
|
'server-id': string
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
// db
|
// db
|
||||||
|
|
||||||
abstract setDbValue(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes>
|
abstract setDbValue(
|
||||||
|
pathArr: Array<string | number>,
|
||||||
|
value: any,
|
||||||
|
): Promise<RR.SetDBValueRes>
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ export abstract class ApiService {
|
|||||||
params: RR.GetPackageMetricsReq,
|
params: RR.GetPackageMetricsReq,
|
||||||
): Promise<RR.GetPackageMetricsRes>
|
): Promise<RR.GetPackageMetricsRes>
|
||||||
|
|
||||||
abstract updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes>
|
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
||||||
|
|
||||||
abstract restartServer(
|
abstract restartServer(
|
||||||
params: RR.RestartServerReq,
|
params: RR.RestartServerReq,
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import { Observable } from 'rxjs'
|
|||||||
import { AuthService } from '../auth.service'
|
import { AuthService } from '../auth.service'
|
||||||
import { DOCUMENT } from '@angular/common'
|
import { DOCUMENT } from '@angular/common'
|
||||||
import { DataModel } from '../patch-db/data-model'
|
import { DataModel } from '../patch-db/data-model'
|
||||||
import { PatchDB, Update } from 'patch-db-client'
|
import { PatchDB, pathFromArray, Update } from 'patch-db-client'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveApiService extends ApiService {
|
export class LiveApiService extends ApiService {
|
||||||
|
readonly eosMarketplaceUrl = 'https://registry.start9.com/'
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DOCUMENT) private readonly document: Document,
|
@Inject(DOCUMENT) private readonly document: Document,
|
||||||
private readonly http: HttpService,
|
private readonly http: HttpService,
|
||||||
@@ -52,7 +54,12 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
// db
|
// 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 })
|
return this.rpcRequest({ method: 'db.put.ui', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,13 +134,11 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'server.metrics', params })
|
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 })
|
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(
|
async restartServer(
|
||||||
@@ -160,12 +165,12 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
// marketplace URLs
|
// 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 })
|
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({
|
return this.rpcRequest({
|
||||||
method: 'marketplace.get',
|
method: 'marketplace.get',
|
||||||
params: { url: fullURL },
|
params: { url: fullUrl },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +180,7 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.marketplaceProxy(
|
return this.marketplaceProxy(
|
||||||
'/eos/v0/latest',
|
'/eos/v0/latest',
|
||||||
params,
|
params,
|
||||||
this.config.marketplace.url,
|
this.eosMarketplaceUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { pauseFor, Log } from '@start9labs/shared'
|
import { pauseFor, Log } from '@start9labs/shared'
|
||||||
import { ApiService } from './embassy-api.service'
|
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 {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
DependencyErrorType,
|
DependencyErrorType,
|
||||||
@@ -15,11 +21,22 @@ import { CifsBackupTarget, RR } from './api.types'
|
|||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
import markdown from 'raw-loader!../../../../../../assets/markdown/md-sample.md'
|
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 { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap'
|
||||||
import { mockPatchData } from './mock-patch'
|
import { mockPatchData } from './mock-patch'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
import { AuthService } from '../auth.service'
|
import { AuthService } from '../auth.service'
|
||||||
|
import { ConnectionService } from '../connection.service'
|
||||||
|
|
||||||
const PROGRESS: InstallProgress = {
|
const PROGRESS: InstallProgress = {
|
||||||
size: 120,
|
size: 120,
|
||||||
@@ -33,28 +50,35 @@ const PROGRESS: InstallProgress = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MockApiService extends ApiService {
|
export class MockApiService extends ApiService {
|
||||||
readonly mockWsSource$ = new BehaviorSubject<Update<DataModel>>({
|
readonly mockWsSource$ = new ReplaySubject<Update<DataModel>>()
|
||||||
id: 1,
|
|
||||||
value: mockPatchData,
|
|
||||||
})
|
|
||||||
private readonly revertTime = 2000
|
private readonly revertTime = 2000
|
||||||
sequence = 0
|
sequence = 0
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly bootstrapper: LocalStorageBootstrap,
|
private readonly bootstrapper: LocalStorageBootstrap,
|
||||||
|
private readonly connectionService: ConnectionService,
|
||||||
private readonly auth: AuthService,
|
private readonly auth: AuthService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.auth.isVerified$.subscribe(verified => {
|
this.auth.isVerified$
|
||||||
if (!verified) {
|
.pipe(
|
||||||
this.patchStream$.next([])
|
tap(() => {
|
||||||
this.mockWsSource$.next({
|
this.sequence = 0
|
||||||
id: 1,
|
this.patchStream$.next([])
|
||||||
value: mockPatchData,
|
}),
|
||||||
})
|
switchMap(verified =>
|
||||||
this.sequence = 0
|
iif(
|
||||||
}
|
() => verified,
|
||||||
})
|
timer(2000).pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.connectionService.websocketConnected$.next(true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
EMPTY,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatic(url: string): Promise<string> {
|
async getStatic(url: string): Promise<string> {
|
||||||
@@ -69,7 +93,12 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
// db
|
// 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)
|
await pauseFor(2000)
|
||||||
const patch = [
|
const patch = [
|
||||||
{
|
{
|
||||||
@@ -198,7 +227,7 @@ export class MockApiService extends ApiService {
|
|||||||
return Mock.getAppMetrics()
|
return Mock.getAppMetrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateServer(params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
|
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const initialProgress = {
|
const initialProgress = {
|
||||||
size: 10000,
|
size: 10000,
|
||||||
@@ -254,10 +283,10 @@ export class MockApiService extends ApiService {
|
|||||||
return {
|
return {
|
||||||
name: 'Dark69',
|
name: 'Dark69',
|
||||||
categories: [
|
categories: [
|
||||||
'featured',
|
|
||||||
'bitcoin',
|
'bitcoin',
|
||||||
'lightning',
|
'lightning',
|
||||||
'data',
|
'data',
|
||||||
|
'featured',
|
||||||
'messaging',
|
'messaging',
|
||||||
'social',
|
'social',
|
||||||
'alt coin',
|
'alt coin',
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ export const mockPatchData: DataModel = {
|
|||||||
'pkg-order': [],
|
'pkg-order': [],
|
||||||
'ack-welcome': '1.0.0',
|
'ack-welcome': '1.0.0',
|
||||||
marketplace: {
|
marketplace: {
|
||||||
'selected-id': '1234',
|
'selected-url': 'https://registry.start9.com/',
|
||||||
'known-hosts': {
|
'known-hosts': {
|
||||||
'1234': {
|
'https://registry.start9.com/': 'Start9 Marketplace',
|
||||||
name: 'Dark9',
|
'https://community-registry.start9.com/': 'Community Marketplace',
|
||||||
url: 'https://test-marketplace.com',
|
'https://dark9-marketplace.com/': 'Dark9',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dev: {},
|
dev: {},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const {
|
|||||||
targetArch,
|
targetArch,
|
||||||
gitHash,
|
gitHash,
|
||||||
useMocks,
|
useMocks,
|
||||||
ui: { api, mocks, marketplace },
|
ui: { api, mocks },
|
||||||
} = require('../../../../../config.json') as WorkspaceConfig
|
} = require('../../../../../config.json') as WorkspaceConfig
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -25,7 +25,6 @@ export class ConfigService {
|
|||||||
targetArch = targetArch
|
targetArch = targetArch
|
||||||
gitHash = gitHash
|
gitHash = gitHash
|
||||||
api = api
|
api = api
|
||||||
marketplace = marketplace
|
|
||||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||||
isConsulate = (window as any)['platform'] === 'ios'
|
isConsulate = (window as any)['platform'] === 'ios'
|
||||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Emver, ErrorToastService } from '@start9labs/shared'
|
import { Emver } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
MarketplacePkg,
|
MarketplacePkg,
|
||||||
AbstractMarketplaceService,
|
AbstractMarketplaceService,
|
||||||
Marketplace,
|
MarketplaceInfo,
|
||||||
FilterPackagesPipe,
|
|
||||||
MarketplaceData,
|
|
||||||
} from '@start9labs/marketplace'
|
} 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 { RR } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
ServerInfo,
|
Manifest,
|
||||||
UIMarketplaceData,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
catchError,
|
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
|
||||||
map,
|
map,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
startWith,
|
startWith,
|
||||||
@@ -28,275 +23,321 @@ import {
|
|||||||
take,
|
take,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs/operators'
|
} 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()
|
@Injectable()
|
||||||
export class MarketplaceService extends AbstractMarketplaceService {
|
export class MarketplaceService implements AbstractMarketplaceService {
|
||||||
private readonly notes = new Map<string, Record<string, string>>()
|
private readonly cache: MasterCache = new Map()
|
||||||
private readonly hasPackages$ = new Subject<boolean>()
|
|
||||||
|
|
||||||
private readonly uiMarketplaceData$ = this.patch
|
private readonly uiMarketplace$: Observable<{ url: string; name: string }> =
|
||||||
.watch$('ui', 'marketplace')
|
this.patch.watch$('ui', 'marketplace').pipe(
|
||||||
.pipe(
|
|
||||||
distinctUntilChanged(
|
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),
|
shareReplay(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly marketplace$ = this.uiMarketplaceData$.pipe(
|
private readonly marketplaceData$: Observable<MarketplaceData> =
|
||||||
map(data => this.toMarketplace(data)),
|
this.uiMarketplace$.pipe(
|
||||||
)
|
switchMap(({ url, name }) =>
|
||||||
|
from(this.loadMarketplace(url)).pipe(
|
||||||
private readonly serverInfo$: Observable<ServerInfo> = this.patch
|
tap(data => {
|
||||||
.watch$('server-info')
|
this.updateName(url, name, data.info!.name)
|
||||||
.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))),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
shareReplay(1),
|
shareReplay(1),
|
||||||
)
|
)
|
||||||
|
|
||||||
private readonly categories$: Observable<Set<string>> =
|
private readonly marketplaceInfo$: Observable<MarketplaceInfo> =
|
||||||
this.registryData$.pipe(
|
this.marketplaceData$.pipe(map(data => data.info!))
|
||||||
map(
|
|
||||||
({ categories }) =>
|
|
||||||
new Set(['featured', 'updates', ...categories, 'all']),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private readonly pkgs$: Observable<MarketplacePkg[]> = this.marketplace$.pipe(
|
private readonly marketplacePkgs$: Observable<MarketplacePkg[]> =
|
||||||
switchMap(({ url }) =>
|
this.marketplaceData$.pipe(map(data => data.packages))
|
||||||
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)
|
|
||||||
|
|
||||||
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),
|
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(
|
constructor(
|
||||||
private readonly api: ApiService,
|
private readonly api: ApiService,
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly config: ConfigService,
|
|
||||||
private readonly errToast: ErrorToastService,
|
|
||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
private readonly filterPkgsPipe: FilterPackagesPipe,
|
) {}
|
||||||
) {
|
|
||||||
super()
|
getUiMarketplace$(): Observable<{ url: string; name: string }> {
|
||||||
|
return this.uiMarketplace$
|
||||||
}
|
}
|
||||||
|
|
||||||
getMarketplace(): Observable<Marketplace> {
|
getMarketplaceInfo$(): Observable<MarketplaceInfo> {
|
||||||
return this.marketplace$
|
return this.marketplaceInfo$
|
||||||
}
|
}
|
||||||
|
|
||||||
getAltMarketplaceData() {
|
getPackages$(): Observable<MarketplacePkg[]> {
|
||||||
return this.uiMarketplaceData$
|
return this.marketplacePkgs$
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategories(): Observable<Set<string>> {
|
getPackage(
|
||||||
return this.categories$
|
id: string,
|
||||||
}
|
version: string,
|
||||||
|
url?: string,
|
||||||
getPackages(): Observable<MarketplacePkg[]> {
|
): Observable<MarketplacePkg | undefined> {
|
||||||
return this.pkgs$
|
return this.uiMarketplace$.pipe(
|
||||||
}
|
switchMap(m => {
|
||||||
|
url = url || m.url
|
||||||
getPackage(id: string, version: string): Observable<MarketplacePkg | null> {
|
if (this.cache.has(url)) {
|
||||||
const params = { ids: [{ id, version }] }
|
const pkg = this.getPkgFromCache(id, version, url)
|
||||||
const fallback$ = this.marketplace$.pipe(
|
if (pkg) return of(pkg)
|
||||||
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 : ''}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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$
|
return this.updates$
|
||||||
}
|
}
|
||||||
|
|
||||||
getReleaseNotes(id: string): Observable<Record<string, string>> {
|
async installPackage(
|
||||||
if (this.notes.has(id)) {
|
id: string,
|
||||||
return of(this.notes.get(id) || {})
|
version: string,
|
||||||
|
url: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const params: RR.InstallPackageReq = {
|
||||||
|
id,
|
||||||
|
'version-spec': `=${version}`,
|
||||||
|
'marketplace-url': url,
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.marketplace$.pipe(
|
await this.api.installPackage(params)
|
||||||
switchMap(({ url }) => this.loadReleaseNotes(id, url)),
|
}
|
||||||
tap(response => this.notes.set(id, response)),
|
|
||||||
catchError(e => {
|
|
||||||
this.errToast.present(e)
|
|
||||||
|
|
||||||
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(
|
fetchPackageMarkdown(
|
||||||
req: Omit<RR.InstallPackageReq, 'marketplace-url'>,
|
id: string,
|
||||||
): Observable<unknown> {
|
type: string,
|
||||||
return this.getMarketplace().pipe(
|
url?: string,
|
||||||
take(1),
|
): Observable<string> {
|
||||||
switchMap(({ url }) =>
|
return this.uiMarketplace$.pipe(
|
||||||
from(
|
switchMap(m => {
|
||||||
this.api.installPackage({
|
return from(
|
||||||
...req,
|
|
||||||
'marketplace-url': url,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getPackageMarkdown(type: string, pkgId: string): Observable<string> {
|
|
||||||
return this.getMarketplace().pipe(
|
|
||||||
switchMap(({ url }) =>
|
|
||||||
from(
|
|
||||||
this.api.marketplaceProxy<string>(
|
this.api.marketplaceProxy<string>(
|
||||||
`/package/v0/${type}/${pkgId}`,
|
`/package/v0/${type}/${id}`,
|
||||||
{},
|
{},
|
||||||
url,
|
url || m.url,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMarketplaceData(
|
private async loadMarketplace(url: string): Promise<MarketplaceData> {
|
||||||
params: RR.GetMarketplaceDataReq,
|
const cachedInfo = this.cache.get(url)?.info
|
||||||
url: string,
|
const [info, packages] = await Promise.all([
|
||||||
): Promise<RR.GetMarketplaceDataRes> {
|
cachedInfo || this.loadInfo(url),
|
||||||
return this.api.marketplaceProxy('/package/v0/info', params, url)
|
this.loadPackages({}, url),
|
||||||
|
])
|
||||||
|
return { info, packages }
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMarketplacePkgs(
|
private async loadInfo(url: string): Promise<MarketplaceInfo> {
|
||||||
params: Omit<RR.GetMarketplacePackagesReq, 'eos-version-compat'>,
|
const info = await this.fetchInfo(url)
|
||||||
url: string,
|
this.updateCache(url, info)
|
||||||
eosVersionCompat: string,
|
return info
|
||||||
): 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 loadReleaseNotes(
|
private async loadPackage(
|
||||||
id: string,
|
id: string,
|
||||||
url: string,
|
url: string,
|
||||||
): Observable<Record<string, string>> {
|
): Promise<MarketplacePkg | undefined> {
|
||||||
return from(
|
const pkgs = await this.loadPackages({ ids: [{ id, version: '*' }] }, url)
|
||||||
this.api.marketplaceProxy<Record<string, string>>(
|
return pkgs[0]
|
||||||
`/package/v0/release-notes/${id}`,
|
}
|
||||||
{},
|
|
||||||
url,
|
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(
|
private async fetchPackage(
|
||||||
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[],
|
|
||||||
id: string,
|
id: string,
|
||||||
version: string,
|
version: string,
|
||||||
): MarketplacePkg | undefined {
|
url: string,
|
||||||
return pkgs.find(pkg => {
|
): Promise<MarketplacePkg | undefined> {
|
||||||
const versionIsSame =
|
const pkgs = await this.fetchPackages({ ids: [{ id, version }] }, url)
|
||||||
version === '*' ||
|
return pkgs[0]
|
||||||
this.emver.compare(pkg.manifest.version, version) === 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 { ModalController } from '@ionic/angular'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
import { filter, share, switchMap, take, tap } from 'rxjs/operators'
|
||||||
import { exists, isEmptyObject } from '@start9labs/shared'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
import { DataModel, UIData } from 'src/app/services/patch-db/data-model'
|
||||||
import { EOSService } from 'src/app/services/eos.service'
|
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(
|
private readonly stream$ = this.connectionService.connected$.pipe(
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
switchMap(() => this.patch.watch$()),
|
switchMap(() => this.patch.watch$()),
|
||||||
filter(obj => exists(obj) && !isEmptyObject(obj)),
|
|
||||||
take(1),
|
take(1),
|
||||||
tap(({ ui }) => {
|
tap(({ ui }) => {
|
||||||
// check for updates to EOS and services
|
// check for updates to EOS and services
|
||||||
@@ -48,8 +46,8 @@ export class PatchDataService extends Observable<DataModel> {
|
|||||||
private checkForUpdates(ui: UIData): void {
|
private checkForUpdates(ui: UIData): void {
|
||||||
if (ui['auto-check-updates'] !== false) {
|
if (ui['auto-check-updates'] !== false) {
|
||||||
this.eosService.getEOS()
|
this.eosService.getEOS()
|
||||||
this.marketplaceService.getPackages().pipe(take(1)).subscribe()
|
this.marketplaceService.getMarketplaceInfo$().pipe(take(1)).subscribe()
|
||||||
this.marketplaceService.getCategories().pipe(take(1)).subscribe()
|
this.marketplaceService.getUpdates$().pipe(take(1)).subscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +62,7 @@ export class PatchDataService extends Observable<DataModel> {
|
|||||||
backdropDismiss: false,
|
backdropDismiss: false,
|
||||||
})
|
})
|
||||||
modal.onWillDismiss().then(() => {
|
modal.onWillDismiss().then(() => {
|
||||||
this.embassyApi
|
this.embassyApi.setDbValue(['ack-welcome'], this.config.version).catch()
|
||||||
.setDbValue({ pointer: '/ack-welcome', value: this.config.version })
|
|
||||||
.catch()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await modal.present()
|
await modal.present()
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ export interface UIData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UIMarketplaceData {
|
export interface UIMarketplaceData {
|
||||||
'selected-id': string | null
|
'selected-url': string
|
||||||
'known-hosts': {
|
'known-hosts': {
|
||||||
[id: string]: {
|
'https://registry.start9.com/': string
|
||||||
url: string
|
'https://community-registry.start9.com/': string
|
||||||
name: string
|
[url: string]: string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,10 +100,7 @@ export class ServerConfigService {
|
|||||||
|
|
||||||
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
saveFns: { [key: string]: (val: any) => Promise<any> } = {
|
||||||
'auto-check-updates': async (enabled: boolean) => {
|
'auto-check-updates': async (enabled: boolean) => {
|
||||||
return this.embassyApi.setDbValue({
|
return this.embassyApi.setDbValue(['auto-check-updates'], enabled)
|
||||||
pointer: '/auto-check-updates',
|
|
||||||
value: 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,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} 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>,
|
patch: PatchDB<DataModel>,
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<PackageDataEntry | undefined> {
|
): Promise<PackageDataEntry | undefined> {
|
||||||
return firstValueFrom(patch.watch$('package-data', id))
|
return firstValueFrom(patch.watch$('package-data', id))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllPackages(
|
export async function getAllPackages(
|
||||||
patch: PatchDB<DataModel>,
|
patch: PatchDB<DataModel>,
|
||||||
): Promise<Record<string, PackageDataEntry>> {
|
): Promise<Record<string, PackageDataEntry>> {
|
||||||
return firstValueFrom(patch.watch$('package-data'))
|
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 { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import { firstValueFrom } from 'rxjs'
|
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'))
|
return firstValueFrom(patch.watch$('server-info'))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user