Hard code registry icons (#1951)

* component for store icons

* order registries and abstract registry urls

* use helper functionm
This commit is contained in:
Matt Hill
2022-11-17 12:23:27 -07:00
committed by Aiden McClelland
parent 4f9fe7245b
commit eec8c41e20
26 changed files with 223 additions and 149 deletions

View File

@@ -6,6 +6,11 @@
"url": "rpc",
"version": "v1"
},
"marketplace": {
"start9": "https://registry.start9.com/",
"community": "https://community-registry.start9.com/",
"beta": "https://beta-registry.start9.com/"
},
"mocks": {
"maskAs": "tor",
"skipStartupAlerts": true

View File

@@ -1,16 +1,10 @@
import { Observable } from 'rxjs'
import {
MarketplacePkg,
Marketplace,
StoreURL,
StoreData,
StoreIdentifier,
} from '../types'
import { MarketplacePkg, Marketplace, StoreData, StoreIdentity } from '../types'
export abstract class AbstractMarketplaceService {
abstract getKnownHosts$(): Observable<Record<StoreURL, StoreIdentifier>>
abstract getKnownHosts$(): Observable<StoreIdentity[]>
abstract getSelectedHost$(): Observable<StoreIdentifier & { url: string }>
abstract getSelectedHost$(): Observable<StoreIdentity>
abstract getMarketplace$(): Observable<Marketplace>

View File

@@ -2,13 +2,11 @@ import { Url } from '@start9labs/shared'
export type StoreURL = string
export type StoreName = string
export type StoreIcon = string // base64
export interface StoreIdentifier {
export interface StoreIdentity {
url: StoreURL
name?: StoreName
icon?: StoreIcon // base64
}
export type Marketplace = Record<StoreURL, StoreData | null>
export interface StoreData {
@@ -18,7 +16,6 @@ export interface StoreData {
export interface StoreInfo {
name: StoreName
icon?: StoreIcon
categories: string[]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -8,6 +8,11 @@ export type WorkspaceConfig = {
url: string
version: string
}
marketplace: {
start9: 'https://registry.start9.com/'
community: 'https://community-registry.start9.com/'
beta: 'https://beta-registry.start9.com/'
}
mocks: {
maskAs: 'tor' | 'lan'
skipStartupAlerts: boolean

View File

@@ -0,0 +1,4 @@
<img *ngIf="url | getIcon as icon; else noIcon" [src]="icon" alt="" />
<ng-template #noIcon>
<ion-icon name="storefront-outline"></ion-icon>
</ng-template>

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { GetIconPipe, StoreIconComponent } from './store-icon.component'
@NgModule({
declarations: [StoreIconComponent, GetIconPipe],
imports: [CommonModule, IonicModule],
exports: [StoreIconComponent],
})
export class StoreIconComponentModule {}

View File

@@ -0,0 +1,36 @@
import {
ChangeDetectionStrategy,
Component,
Input,
Pipe,
PipeTransform,
} from '@angular/core'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'store-icon',
templateUrl: './store-icon.component.html',
styleUrls: ['./store-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StoreIconComponent {
@Input() url: string = ''
}
@Pipe({
name: 'getIcon',
})
export class GetIconPipe implements PipeTransform {
constructor(private readonly config: ConfigService) {}
transform(url: string): string | null {
const { start9, community } = this.config.marketplace
if (url === start9) {
return 'assets/img/icon.png'
} else if (url === community) {
return 'assets/img/community-store.png'
}
return null
}
}

View File

@@ -3,9 +3,15 @@ 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],
imports: [
CommonModule,
IonicModule,
SharedPipesModule,
StoreIconComponentModule,
],
declarations: [MarketplaceSettingsPage],
})
export class MarketplaceSettingsPageModule {}

View File

@@ -10,23 +10,23 @@
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group *ngIf="marketplace$ | async as m">
<ion-item-group *ngIf="stores$ | async as stores">
<ion-item-divider>Default Registries</ion-item-divider>
<ion-item
*ngFor="let s of m.standard"
*ngFor="let s of stores.standard"
detail="false"
[button]="s.url !== m.selected"
(click)="s.url === m.selected ? '' : presentAction(s)"
[button]="!s.selected"
(click)="s.selected ? '' : presentAction(s)"
>
<ion-avatar slot="start">
<img [src]="'data:image/png;base64,' + s.icon | trustUrl" alt="" />
<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.url === m.selected"
*ngIf="s.selected"
slot="end"
size="large"
name="checkmark"
@@ -45,18 +45,22 @@
</ion-item>
<ion-item
*ngFor="let a of m.alt"
*ngFor="let a of stores.alt"
detail="false"
[button]="a.url !== m.selected"
(click)="a.url === m.selected ? '' : presentAction(a, true)"
[button]="!a.selected"
(click)="a.selected ? '' : presentAction(a, true)"
>
<ion-icon slot="start" name="storefront-outline"></ion-icon>
<store-icon
slot="start"
[url]="a.url"
style="font-size: 36px"
></store-icon>
<ion-label>
<h2>{{ a.name }}</h2>
<p>{{ a.url }}</p>
</ion-label>
<ion-icon
*ngIf="a.url === m.selected"
*ngIf="a.selected"
slot="end"
size="large"
name="checkmark"

View File

@@ -7,18 +7,15 @@ import {
} from '@ionic/angular'
import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService } from '@start9labs/shared'
import {
AbstractMarketplaceService,
StoreIdentifier,
} from '@start9labs/marketplace'
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 'src/app/services/patch-db/data-model'
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 { firstValueFrom } from 'rxjs'
import { combineLatest, firstValueFrom } from 'rxjs'
@Component({
selector: 'marketplace-settings',
@@ -27,24 +24,21 @@ import { firstValueFrom } from 'rxjs'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MarketplaceSettingsPage {
marketplace$ = this.patch.watch$('ui', 'marketplace').pipe(
map(m => {
const selected = m['selected-url']
const hosts = Object.entries(m['known-hosts'])
stores$ = combineLatest([
this.marketplaceService.getKnownHosts$(),
this.marketplaceService.getSelectedHost$(),
]).pipe(
map(([stores, selected]) => {
const hmmm = stores.map(s => ({
...s,
selected: s.url === selected.url,
}))
// 0 and 1 are prod and community
const standard = hmmm.slice(0, 2)
// 2 and beyond are alts
const alt = hmmm.slice(2)
const standard = hosts
.map(([url, info]) => {
return { url, ...info }
})
.slice(0, 2) // 0 and 1 will always be prod and community
const alt = hosts
.map(([url, info]) => {
return { url, ...info }
})
.slice(2) // 2 and beyond will always be alts
return { selected, standard, alt }
return { standard, alt }
}),
)
@@ -207,16 +201,16 @@ export class MarketplaceSettingsPage {
loader.message = 'Validating marketplace...'
await loader.present()
const { name, icon } = await firstValueFrom(
const { name } = await firstValueFrom(
this.marketplaceService.fetchInfo$(url),
)
// Save
loader.message = 'Saving...'
await this.api.setDbValue<StoreIdentifier>(
await this.api.setDbValue<{ name: string }>(
['marketplace', 'known-hosts', url],
{ name, icon },
{ name },
)
}
@@ -230,7 +224,7 @@ export class MarketplaceSettingsPage {
this.patch.watch$('ui', 'marketplace', 'known-hosts'),
)
const filtered: { [url: string]: StoreIdentifier } = Object.keys(hosts)
const filtered: { [url: string]: UIStore } = Object.keys(hosts)
.filter(key => key !== url)
.reduce((prev, curr) => {
const name = hosts[curr]
@@ -241,7 +235,7 @@ export class MarketplaceSettingsPage {
}, {})
try {
await this.api.setDbValue<{ [url: string]: StoreIdentifier }>(
await this.api.setDbValue<{ [url: string]: UIStore }>(
['marketplace', 'known-hosts'],
filtered,
)

View File

@@ -15,6 +15,7 @@ import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/b
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'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
const routes: Routes = [
{
@@ -39,6 +40,7 @@ const routes: Routes = [
SearchModule,
SkeletonModule,
MarketplaceSettingsPageModule,
StoreIconComponentModule,
],
declarations: [MarketplaceListPage],
exports: [MarketplaceListPage],

View File

@@ -24,14 +24,7 @@
<ion-row>
<ion-col size="12">
<div class="heading">
<img
*ngIf="details.icon; else noIcon"
[src]="'data:image/png;base64,' + details.icon | trustUrl"
alt=""
/>
<ng-template #noIcon>
<ion-icon name="storefront-outline"></ion-icon>
</ng-template>
<store-icon class="icon" [url]="details.url"></store-icon>
<h1 class="montserrat ion-text-center">{{ details.name }}</h1>
</div>
<ion-button fill="clear" (click)="presentModalMarketplaceSettings()">

View File

@@ -1,15 +1,14 @@
.heading {
$icon-size: 64px;
margin-top: 32px;
img {
max-width: $icon-size;
}
h1 {
font-size: 42px;
}
ion-icon {
font-size: $icon-size;
}
}
.icon {
display: inline-block;
max-width: 80px;
font-size: 80px;
}
.divider {

View File

@@ -4,6 +4,7 @@ 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 { ConfigService } from 'src/app/services/config.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -30,25 +31,26 @@ export class MarketplaceListPage {
readonly localPkgs$ = this.patch.watch$('package-data')
readonly details$ = this.marketplaceService.getSelectedHost$().pipe(
map(({ url, name, icon }) => {
map(({ url, name }) => {
const { start9, community, beta } = this.config.marketplace
let color: string
let description: string
switch (url) {
case 'https://registry.start9.com/':
case start9:
color = 'success'
description =
'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have a questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.'
break
case 'https://beta-registry.start9.com/':
color = 'primary'
description =
'Services from this registry are undergoing active testing and may contain bugs. <b>Install at your own risk</b>. If you discover a bug or have a suggestion for improvement, please report it to the Start9 team in our community testing channel on Matrix.'
break
case 'https://community-registry.start9.com/':
case community:
color = 'tertiary'
description =
'Services from this registry 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.'
break
case beta:
color = 'primary'
description =
'Services from this registry 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
default:
// alt marketplace
color = 'warning'
@@ -59,7 +61,6 @@ export class MarketplaceListPage {
return {
name,
url,
icon,
color,
description,
}
@@ -71,6 +72,7 @@ export class MarketplaceListPage {
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly modalCtrl: ModalController,
private readonly config: ConfigService,
) {}
category = 'featured'

View File

@@ -8,6 +8,7 @@ import { MarkdownPipeModule, SharedPipesModule } from '@start9labs/shared'
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
import { RoundProgressModule } from 'angular-svg-round-progressbar'
import { InstallProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
const routes: Routes = [
{
@@ -27,6 +28,7 @@ const routes: Routes = [
MarkdownPipeModule,
RoundProgressModule,
InstallProgressPipeModule,
StoreIconComponentModule,
],
declarations: [UpdatesPage, FilterUpdatesPipe],
})

View File

@@ -9,26 +9,24 @@
<ion-content class="ion-padding">
<ion-item-group *ngIf="data$ | async as data">
<ng-container *ngFor="let host of data.hosts | keyvalue">
<ng-container *ngFor="let host of data.hosts">
<ion-item-divider>
{{ host.value.name }} &nbsp;
<img
style="max-width: 24px"
[src]="'data:image/png;base64,' + host.value.icon | trustUrl"
alt=""
/>
{{ host.name }} &nbsp;
<div style="max-width: 16px">
<store-icon [url]="host.url"></store-icon>
</div>
</ion-item-divider>
<div class="ion-padding-start ion-padding-bottom">
<ion-item *ngIf="data.errors.includes(host.key)">
<ion-item *ngIf="data.errors.includes(host.url)">
<ion-text color="danger">Request Failed</ion-text>
</ion-item>
<ng-container
*ngIf="data.marketplace[host.key]?.packages as packages else loading"
*ngIf="data.marketplace[host.url]?.packages as packages else loading"
>
<ng-container
*ngIf="packages | filterUpdates : data.localPkgs : host.key as updates"
*ngIf="packages | filterUpdates : data.localPkgs : host.url as updates"
>
<ion-item *ngFor="let pkg of updates">
<ng-container *ngIf="data.localPkgs[pkg.manifest.id] as local">
@@ -64,7 +62,7 @@
></ion-spinner>
<ng-template #updateBtn>
<ion-button
(click)="update(pkg.manifest.id, host.key)"
(click)="update(pkg.manifest.id, host.url)"
color="dark"
strong
>

View File

@@ -12,7 +12,7 @@ import {
Marketplace,
MarketplaceManifest,
MarketplacePkg,
StoreIdentifier,
StoreIdentity,
} from '@start9labs/marketplace'
import { Emver } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core'
@@ -20,7 +20,7 @@ import { combineLatest, Observable } from 'rxjs'
import { PrimaryRendering } from '../../services/pkg-status-rendering.service'
interface UpdatesData {
hosts: Record<string, StoreIdentifier>
hosts: StoreIdentity[]
marketplace: Marketplace
localPkgs: Record<string, PackageDataEntry>
errors: string[]

File diff suppressed because one or more lines are too long

View File

@@ -22,8 +22,6 @@ import { PatchDB, pathFromArray, Update } from 'patch-db-client'
@Injectable()
export class LiveApiService extends ApiService {
readonly eosMarketplaceUrl = 'https://registry.start9.com/'
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly http: HttpService,
@@ -136,7 +134,7 @@ export class LiveApiService extends ApiService {
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
const params = {
'marketplace-url': url || this.eosMarketplaceUrl,
'marketplace-url': url || this.config.marketplace.start9,
}
return this.rpcRequest({ method: 'server.update', params })
}
@@ -180,7 +178,7 @@ export class LiveApiService extends ApiService {
return this.marketplaceProxy(
'/eos/v0/latest',
params,
this.eosMarketplaceUrl,
this.config.marketplace.start9,
)
}

View File

@@ -38,7 +38,6 @@ import { WebSocketSubjectConfig } from 'rxjs/webSocket'
import { AuthService } from '../auth.service'
import { ConnectionService } from '../connection.service'
import { StoreInfo } from '@start9labs/marketplace'
import { COMMUNITY_REGISTRY, START9_REGISTRY } from './api-icons'
const PROGRESS: InstallProgress = {
size: 120,
@@ -284,7 +283,6 @@ export class MockApiService extends ApiService {
if (path === '/package/v0/info') {
const info: StoreInfo = {
name: 'Start9 Registry',
icon: START9_REGISTRY,
categories: [
'bitcoin',
'lightning',

View File

@@ -7,24 +7,19 @@ import {
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { COMMUNITY_REGISTRY, START9_REGISTRY } from './api-icons'
import { Mock } from './api.fixures'
export const mockPatchData: DataModel = {
ui: {
name: `Matt's Embassy`,
'pkg-order': [],
'ack-welcome': '1.0.0',
marketplace: {
'selected-url': 'https://registry.start9.com/',
'known-hosts': {
'https://registry.start9.com/': {
name: 'Start9 Registry',
icon: START9_REGISTRY,
},
'https://community-registry.start9.com/': {
icon: COMMUNITY_REGISTRY,
},
'https://community-registry.start9.com/': {},
'https://dark9-marketplace.com/': {
name: 'Dark9',
},

View File

@@ -12,7 +12,7 @@ const {
targetArch,
gitHash,
useMocks,
ui: { api, mocks },
ui: { api, marketplace, mocks },
} = require('../../../../../config.json') as WorkspaceConfig
@Injectable({
@@ -28,6 +28,7 @@ export class ConfigService {
targetArch = targetArch
gitHash = gitHash
api = api
marketplace = marketplace
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
isConsulate = (window as any)['platform'] === 'ios'
supportsWebSockets = !!window.WebSocket || this.isConsulate

View File

@@ -5,7 +5,7 @@ import {
StoreData,
Marketplace,
StoreInfo,
StoreIdentifier,
StoreIdentity,
} from '@start9labs/marketplace'
import {
BehaviorSubject,
@@ -19,7 +19,7 @@ import {
} from 'rxjs'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { DataModel, UIStore } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import {
catchError,
@@ -29,35 +29,53 @@ import {
shareReplay,
startWith,
switchMap,
take,
tap,
} from 'rxjs/operators'
import { getNewEntries } from '@start9labs/shared'
import { ConfigService } from './config.service'
@Injectable()
export class MarketplaceService implements AbstractMarketplaceService {
private readonly knownHosts$ = this.patch.watch$(
'ui',
'marketplace',
'known-hosts',
)
private readonly knownHosts$: Observable<StoreIdentity[]> = this.patch
.watch$('ui', 'marketplace', 'known-hosts')
.pipe(
map(hosts => {
const { start9, community } = this.config.marketplace
let arr = [
toStoreIdentity(start9, hosts[start9]),
toStoreIdentity(community, hosts[community]),
]
private readonly selectedHost$ = this.patch.watch$('ui', 'marketplace').pipe(
distinctUntilKeyChanged('selected-url'),
map(data => ({
url: data['selected-url'],
...data['known-hosts'][data['selected-url']],
})),
shareReplay(1),
)
return arr.concat(
Object.entries(hosts)
.filter(([url, _]) => ![start9, community].includes(url as any))
.map(([url, store]) => toStoreIdentity(url, store)),
)
}),
)
private readonly selectedHost$: Observable<StoreIdentity> = this.patch
.watch$('ui', 'marketplace')
.pipe(
distinctUntilKeyChanged('selected-url'),
map(({ 'selected-url': url, 'known-hosts': hosts }) =>
toStoreIdentity(url, hosts[url]),
),
shareReplay(1),
)
private readonly marketplace$ = this.knownHosts$.pipe(
startWith<Record<string, StoreIdentifier>>({}),
startWith<StoreIdentity[]>([]),
pairwise(),
mergeMap(([prev, curr]) => from(Object.entries(getNewEntries(prev, curr)))),
mergeMap(([url, registry]) =>
mergeMap(([prev, curr]) =>
curr.filter(c => !prev.find(p => c.url === p.url)),
),
mergeMap(({ url, name }) =>
this.fetchStore$(url).pipe(
tap(data => {
if (data?.info) this.updateStoreName(url, name, data.info.name)
}),
map<StoreData | null, [string, StoreData | null]>(data => {
if (data?.info) this.updateStoreIdentifier(url, registry, data.info)
return [url, data]
}),
startWith<[string, StoreData | null]>([url, null]),
@@ -74,22 +92,30 @@ export class MarketplaceService implements AbstractMarketplaceService {
shareReplay(1),
)
private readonly selectedStore$ = this.selectedHost$.pipe(
switchMap(({ url }) => this.marketplace$.pipe(map(m => m[url]))),
)
private readonly selectedStore$: Observable<StoreData | null> =
this.selectedHost$.pipe(
switchMap(({ url }) =>
this.marketplace$.pipe(
filter(m => !!m[url]),
take(1),
map(m => m[url]),
),
),
)
private readonly requestErrors$ = new BehaviorSubject<string[]>([])
constructor(
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
) {}
getKnownHosts$(): Observable<Record<string, StoreIdentifier>> {
getKnownHosts$(): Observable<StoreIdentity[]> {
return this.knownHosts$
}
getSelectedHost$(): Observable<StoreIdentifier & { url: string }> {
getSelectedHost$(): Observable<StoreIdentity> {
return this.selectedHost$
}
@@ -234,16 +260,23 @@ export class MarketplaceService implements AbstractMarketplaceService {
)
}
private async updateStoreIdentifier(
private async updateStoreName(
url: string,
oldInfo: StoreIdentifier,
newInfo: StoreIdentifier,
oldName: string | undefined,
newName: string,
): Promise<void> {
if (oldInfo.name !== newInfo.name || oldInfo.icon !== newInfo.icon) {
this.api.setDbValue<StoreIdentifier>(
['marketplace', 'known-hosts', url],
newInfo,
if (oldName !== newName) {
this.api.setDbValue<string>(
['marketplace', 'known-hosts', url, 'name'],
newName,
)
}
}
}
function toStoreIdentity(url: string, uiStore: UIStore): StoreIdentity {
return {
url,
...uiStore,
}
}

View File

@@ -1,6 +1,6 @@
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { Url } from '@start9labs/shared'
import { MarketplaceManifest, StoreIdentifier } from '@start9labs/marketplace'
import { MarketplaceManifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
export interface DataModel {
@@ -11,7 +11,6 @@ export interface DataModel {
export interface UIData {
name: string | null
'pkg-order': string[]
'ack-welcome': string // eOS emver
marketplace: UIMarketplaceData
dev: DevData
@@ -26,12 +25,16 @@ export interface UIData {
export interface UIMarketplaceData {
'selected-url': string
'known-hosts': {
'https://registry.start9.com/': StoreIdentifier
'https://community-registry.start9.com/': StoreIdentifier
[url: string]: StoreIdentifier
'https://registry.start9.com/': UIStore
'https://community-registry.start9.com/': UIStore
[url: string]: UIStore
}
}
export interface UIStore {
name?: string
}
export interface DevData {
[id: string]: DevProjectData
}