mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
move marketplace settings into marketplace tab (#1895)
* move marketplace settings into marketplace tab * Update frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>
This commit is contained in:
committed by
Aiden McClelland
parent
17d39143ac
commit
e5aeced045
@@ -14,6 +14,7 @@ import {
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||
import { MarketplaceListPage } from './marketplace-list.page'
|
||||
import { MarketplaceSettingsPageModule } from 'src/app/modals/marketplace-settings/marketplace-settings.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -37,6 +38,7 @@ const routes: Routes = [
|
||||
CategoriesModule,
|
||||
SearchModule,
|
||||
SkeletonModule,
|
||||
MarketplaceSettingsPageModule,
|
||||
],
|
||||
declarations: [MarketplaceListPage],
|
||||
exports: [MarketplaceListPage],
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<ion-row>
|
||||
<ion-col size="12">
|
||||
<h1 class="heading montserrat ion-text-center">{{ details.name }}</h1>
|
||||
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">
|
||||
<ion-icon slot="start" name="repeat-outline"></ion-icon>
|
||||
Switch Marketplaces
|
||||
</ion-button>
|
||||
<marketplace-search [(query)]="query"></marketplace-search>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { MarketplaceSettingsPage } from 'src/app/modals/marketplace-settings/marketplace-settings.page'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@@ -67,11 +69,22 @@ export class MarketplaceListPage {
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
) {}
|
||||
|
||||
category = 'featured'
|
||||
query = ''
|
||||
|
||||
async presentModalMarketplaceSettings() {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: MarketplaceSettingsPage,
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
onCategoryChange(category: string): void {
|
||||
this.category = category
|
||||
this.query = ''
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { MarketplacesPage } from './marketplaces.page'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: MarketplacesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
],
|
||||
declarations: [MarketplacesPage],
|
||||
})
|
||||
export class MarketplacesPageModule {}
|
||||
@@ -1,67 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="embassy"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Marketplace Settings</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-item-group *ngIf="marketplace$ | async as m">
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
Connect to a standard marketplaces or an alternative marketplace.
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Standard Marketplaces</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let s of m.standard"
|
||||
detail="false"
|
||||
[button]="s.url !== m.selected"
|
||||
(click)="s.url === m.selected ? '' : presentAction(s)"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="s.url === m.selected"
|
||||
slot="end"
|
||||
size="large"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ s.name }}</h2>
|
||||
<p>{{ s.url }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Alt Marketplaces</ion-item-divider>
|
||||
<ion-item button detail="false" (click)="presentModalAdd()">
|
||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
||||
<ion-label>
|
||||
<ion-text color="dark">
|
||||
<b>Add Alt Marketplace</b>
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item
|
||||
*ngFor="let a of m.alt"
|
||||
detail="false"
|
||||
[button]="a.url !== m.selected"
|
||||
(click)="a.url === m.selected ? '' : presentAction(a, true)"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="a.url === m.selected"
|
||||
slot="end"
|
||||
size="large"
|
||||
name="checkmark"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ a.name }}</h2>
|
||||
<p>{{ a.url }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,12 +0,0 @@
|
||||
.skeleton-parts {
|
||||
padding-bottom: 6px;
|
||||
|
||||
ion-button::part(native) {
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
padding-right: 32px;
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import {
|
||||
ActionSheetController,
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { ActionSheetButton } from '@ionic/core'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ValueSpecObject } from 'src/app/pkg-config/config-types'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from '../../../services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplaces',
|
||||
templateUrl: 'marketplaces.page.html',
|
||||
styleUrls: ['marketplaces.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplacesPage {
|
||||
marketplace$ = this.patch.watch$('ui', 'marketplace').pipe(
|
||||
map(m => {
|
||||
const selected = m['selected-url']
|
||||
const hosts = Object.entries(m['known-hosts'])
|
||||
|
||||
const standard = hosts
|
||||
.map(([url, name]) => {
|
||||
return { url, name }
|
||||
})
|
||||
.slice(0, 2) // 0 and 1 will always be prod and community
|
||||
|
||||
const alt = hosts
|
||||
.map(([url, name]) => {
|
||||
return { url, name }
|
||||
})
|
||||
.slice(2) // 2 and beyond will always be alts
|
||||
|
||||
return { selected, standard, alt }
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
@Inject(AbstractMarketplaceService)
|
||||
private readonly marketplaceService: MarketplaceService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {}
|
||||
|
||||
async presentModalAdd() {
|
||||
const { name, spec } = getMarketplaceValueSpec()
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: name,
|
||||
spec,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save for Later',
|
||||
handler: (value: { url: string }) => {
|
||||
this.saveOnly(new URL(value.url))
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Save and Connect',
|
||||
handler: (value: { url: string }) => {
|
||||
this.saveAndConnect(new URL(value.url))
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAction(
|
||||
{ url, name }: { url: string; name: string },
|
||||
canDelete = false,
|
||||
) {
|
||||
const buttons: ActionSheetButton[] = [
|
||||
{
|
||||
text: 'Connect',
|
||||
handler: () => {
|
||||
this.connect(url)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (canDelete) {
|
||||
buttons.unshift({
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
handler: () => {
|
||||
this.presentAlertDelete(url, name)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const action = await this.actionCtrl.create({
|
||||
header: name,
|
||||
mode: 'ios',
|
||||
buttons,
|
||||
})
|
||||
|
||||
await action.present()
|
||||
}
|
||||
|
||||
private async presentAlertDelete(url: string, name: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to delete ${name}?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
handler: () => this.delete(url),
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async connect(
|
||||
url: string,
|
||||
loader?: HTMLIonLoadingElement,
|
||||
): Promise<void> {
|
||||
const message = 'Changing Marketplace...'
|
||||
if (!loader) {
|
||||
loader = await this.loadingCtrl.create({ message })
|
||||
await loader.present()
|
||||
} else {
|
||||
loader.message = message
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.setDbValue(['marketplace', 'selected-url'], url)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveOnly(url: URL): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create()
|
||||
|
||||
try {
|
||||
await this.validateAndSave(url, loader)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAndConnect(url: URL): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create()
|
||||
|
||||
try {
|
||||
await this.validateAndSave(url, loader)
|
||||
await this.connect(url.toString(), loader)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async validateAndSave(
|
||||
urlObj: URL,
|
||||
loader: HTMLIonLoadingElement,
|
||||
): Promise<void> {
|
||||
const url = urlObj.toString()
|
||||
// Error on duplicates
|
||||
const hosts = await firstValueFrom(
|
||||
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
|
||||
)
|
||||
const currentUrls = Object.keys(hosts)
|
||||
if (currentUrls.includes(url)) throw new Error('marketplace already added')
|
||||
|
||||
// Validate
|
||||
loader.message = 'Validating marketplace...'
|
||||
await loader.present()
|
||||
|
||||
const name = await firstValueFrom(this.marketplaceService.fetchInfo$(url))
|
||||
|
||||
// Save
|
||||
loader.message = 'Saving...'
|
||||
|
||||
await this.api.setDbValue(['marketplace', 'known-hosts', url], name)
|
||||
}
|
||||
|
||||
private async delete(url: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Deleting...',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
const hosts = await firstValueFrom(
|
||||
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
|
||||
)
|
||||
|
||||
const filtered = Object.keys(hosts)
|
||||
.filter(key => key !== url)
|
||||
.reduce((prev, curr) => {
|
||||
const name = hosts[curr]
|
||||
return {
|
||||
...prev,
|
||||
[curr]: name,
|
||||
}
|
||||
}, {})
|
||||
|
||||
try {
|
||||
await this.api.setDbValue(['marketplace', 'known-hosts'], filtered)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMarketplaceValueSpec(): ValueSpecObject {
|
||||
return {
|
||||
type: 'object',
|
||||
name: 'Add Marketplace',
|
||||
spec: {
|
||||
url: {
|
||||
type: 'string',
|
||||
name: 'URL',
|
||||
description: 'The fully-qualified URL of the alt marketplace.',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`,
|
||||
'pattern-description': 'Must be a valid URL',
|
||||
placeholder: 'e.g. https://example.org',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -34,13 +34,6 @@ const routes: Routes = [
|
||||
m => m.KernelLogsPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'marketplaces',
|
||||
loadChildren: () =>
|
||||
import('./marketplaces/marketplaces.module').then(
|
||||
m => m.MarketplacesPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'metrics',
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -424,17 +424,6 @@ export class ServerShowPage {
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Marketplace Settings',
|
||||
description: 'Add or remove marketplaces',
|
||||
icon: 'storefront-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['marketplaces'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
Insights: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user