rename frontend to web and update contributing guide (#2509)

* rename frontend to web and update contributing guide

* rename this time

* fix build

* restructure rust code

* update documentation

* update descriptions

* Update CONTRIBUTING.md

Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
Matt Hill
2023-11-13 14:22:23 -07:00
committed by GitHub
parent 871f78b570
commit 86567e7fa5
968 changed files with 812 additions and 6672 deletions

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { MarketplaceSettingsPage } from './marketplace-settings.page'
import { SharedPipesModule } from '@start9labs/shared'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
@NgModule({
imports: [
CommonModule,
IonicModule,
SharedPipesModule,
StoreIconComponentModule,
],
declarations: [MarketplaceSettingsPage],
})
export class MarketplaceSettingsPageModule {}

View File

@@ -0,0 +1,67 @@
<ion-header>
<ion-toolbar>
<ion-title>Change Registry</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group *ngIf="stores$ | async as stores">
<ion-item-divider>Default Registries</ion-item-divider>
<ion-item
*ngFor="let s of stores.standard"
detail="false"
[button]="!s.selected"
(click)="s.selected ? '' : presentAction(s)"
>
<ion-avatar slot="start">
<store-icon [url]="s.url"></store-icon>
</ion-avatar>
<ion-label>
<h2>{{ s.name }}</h2>
<p>{{ s.url }}</p>
</ion-label>
<ion-icon
*ngIf="s.selected"
slot="end"
size="large"
name="checkmark"
color="success"
></ion-icon>
</ion-item>
<ion-item-divider>Custom Registries</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 custom registry</b>
</ion-text>
</ion-label>
</ion-item>
<ion-item
*ngFor="let a of stores.alt"
detail="false"
[button]="!a.selected"
(click)="a.selected ? '' : presentAction(a, true)"
>
<store-icon slot="start" [url]="a.url" size="36px"></store-icon>
<ion-label>
<h2>{{ a.name }}</h2>
<p>{{ a.url }}</p>
</ion-label>
<ion-icon
*ngIf="a.selected"
slot="end"
size="large"
name="checkmark"
color="success"
></ion-icon>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,276 @@
import {
ChangeDetectionStrategy,
Component,
Inject,
ViewChild,
} from '@angular/core'
import {
ActionSheetController,
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService, sameUrl, toUrl } 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, UIStore } from 'src/app/services/patch-db/data-model'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { map } from 'rxjs/operators'
import { combineLatest, firstValueFrom } from 'rxjs'
@Component({
selector: 'marketplace-settings',
templateUrl: 'marketplace-settings.page.html',
styleUrls: ['marketplace-settings.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceSettingsPage {
stores$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getSelectedHost$(),
]).pipe(
map(([stores, selected]) => {
const toSlice = stores.map(s => ({
...s,
selected: sameUrl(s.url, selected.url),
}))
// 0 and 1 are prod and community
const standard = toSlice.slice(0, 2)
// 2 and beyond are alts
const alt = toSlice.slice(2)
return { 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 dismiss() {
this.modalCtrl.dismiss()
}
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(value.url)
},
},
{
text: 'Save and Connect',
handler: (value: { url: string }) => {
this.saveAndConnect(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 Registry...'
if (!loader) {
loader = await this.loadingCtrl.create({ message })
await loader.present()
} else {
loader.message = message
}
try {
await this.api.setDbValue<string>(['marketplace', 'selected-url'], url)
} catch (e: any) {
this.errToast.present(e)
} finally {
loader.dismiss()
this.dismiss()
}
}
private async saveOnly(rawUrl: string): Promise<void> {
const loader = await this.loadingCtrl.create()
try {
const url = new URL(rawUrl).toString()
await this.validateAndSave(url, loader)
} catch (e: any) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
private async saveAndConnect(rawUrl: string): Promise<void> {
const loader = await this.loadingCtrl.create()
try {
const url = new URL(rawUrl).toString()
await this.validateAndSave(url, loader)
await this.connect(url, loader)
} catch (e: any) {
this.errToast.present(e)
} finally {
loader.dismiss()
this.dismiss()
}
}
private async validateAndSave(
url: string,
loader: HTMLIonLoadingElement,
): Promise<void> {
// Error on duplicates
const hosts = await firstValueFrom(
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
)
const currentUrls = Object.keys(hosts).map(toUrl)
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<{ name: string }>(
['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: { [url: string]: UIStore } = Object.keys(hosts)
.filter(key => !sameUrl(key, url))
.reduce((prev, curr) => {
const name = hosts[curr]
return {
...prev,
[curr]: name,
}
}, {})
try {
await this.api.setDbValue<{ [url: string]: UIStore }>(
['marketplace', 'known-hosts'],
filtered,
)
} catch (e: any) {
this.errToast.present(e)
} finally {
loader.dismiss()
}
}
}
function getMarketplaceValueSpec(): ValueSpecObject {
return {
type: 'object',
name: 'Add Custom Registry',
spec: {
url: {
type: 'string',
name: 'URL',
description: 'A fully-qualified URL of the custom registry',
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',
},
},
}
}