update FE types and unify sideload page with marketplace show

This commit is contained in:
Matt Hill
2023-03-07 14:37:14 -07:00
committed by Aiden McClelland
parent 6556fcc531
commit cb7790ccba
57 changed files with 699 additions and 1780 deletions

View File

@@ -1,4 +0,0 @@
.all-notes {
position: absolute;
right: 10px;
}

View File

@@ -3,8 +3,8 @@
<div class="text"> <div class="text">
<h1 ticker class="title">{{ pkg.manifest.title }}</h1> <h1 ticker class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p> <p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<p class="published"> <p *ngIf="pkg['published-at'] as published" class="published">
Released: {{ pkg['published-at'] | date: 'medium' }} Released: {{ published | date : 'medium' }}
</p> </p>
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@@ -23,7 +23,7 @@ export interface MarketplacePkg {
icon: Url icon: Url
license: Url license: Url
instructions: Url instructions: Url
manifest: MarketplaceManifest manifest: Manifest
categories: string[] categories: string[]
versions: string[] versions: string[]
'dependency-metadata': { 'dependency-metadata': {
@@ -38,7 +38,7 @@ export interface DependencyMetadata {
hidden: boolean hidden: boolean
} }
export interface MarketplaceManifest<T = unknown> { export interface Manifest {
id: string id: string
title: string title: string
version: string version: string
@@ -52,7 +52,7 @@ export interface MarketplaceManifest<T = unknown> {
} }
replaces?: string[] replaces?: string[]
'release-notes': string 'release-notes': string
license: string // type of license license: string // name of license
'wrapper-repo': Url 'wrapper-repo': Url
'upstream-repo': Url 'upstream-repo': Url
'support-site': Url 'support-site': Url
@@ -65,10 +65,11 @@ export interface MarketplaceManifest<T = unknown> {
start: string | null start: string | null
stop: string | null stop: string | null
} }
dependencies: Record<string, Dependency<T>> dependencies: Record<string, Dependency>
'os-version': string
} }
export interface Dependency<T> { export interface Dependency {
version: string version: string
requirement: requirement:
| { | {
@@ -83,5 +84,4 @@ export interface Dependency<T> {
type: 'required' type: 'required'
} }
description: string | null description: string | null
config: T
} }

View File

@@ -53,3 +53,5 @@ export function toUrl(text: string | null | undefined): string {
return '' return ''
} }
} }
export type WithId<T> = T & { id: string }

View File

@@ -94,10 +94,7 @@ export class MenuComponent {
Object.entries(marketplace).reduce((list, [_, store]) => { Object.entries(marketplace).reduce((list, [_, store]) => {
store?.packages.forEach(({ manifest: { id, version } }) => { store?.packages.forEach(({ manifest: { id, version } }) => {
if ( if (
this.emver.compare( this.emver.compare(version, local[id]?.manifest.version || '') === 1
version,
local[id]?.installed?.manifest.version || '',
) === 1
) )
list.add(id) list.add(id)
}) })

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { AnyLinkComponent } from './any-link.component' import { AnyLinkComponent } from './any-link.component'
@NgModule({ @NgModule({

View File

@@ -9,13 +9,6 @@
<span *ngIf="!installProgress"> <span *ngIf="!installProgress">
{{ (connected$ | async) ? rendering.display : 'Unknown' }} {{ (connected$ | async) ? rendering.display : 'Unknown' }}
<span *ngIf="rendering.showDots" class="loading-dots"></span> <span *ngIf="rendering.showDots" class="loading-dots"></span>
<span
*ngIf="
rendering.display === PR[PS.Stopping].display &&
(sigtermTimeout | durationToSeconds) > 30
"
>this may take a while</span
>
</span> </span>
<span *ngIf="installProgress"> <span *ngIf="installProgress">
<ion-text <ion-text
@@ -23,7 +16,8 @@
color="primary" color="primary"
> >
Installing Installing
<span class="loading-dots"></span>{{ progress }} <span class="loading-dots"></span>
{{ progress }}
</ion-text> </ion-text>
</span> </span>
</p> </p>

View File

@@ -21,7 +21,6 @@ export class StatusComponent {
@Input() style?: string = 'regular' @Input() style?: string = 'regular'
@Input() weight?: string = 'normal' @Input() weight?: string = 'normal'
@Input() installProgress?: InstallProgress @Input() installProgress?: InstallProgress
@Input() sigtermTimeout?: string | null = null
readonly connected$ = this.connectionService.connected$ readonly connected$ = this.connectionService.connected$

View File

@@ -20,7 +20,7 @@
<ng-template #notLoading> <ng-template #notLoading>
<ion-item *ngIf="loadingError; else noError"> <ion-item *ngIf="loadingError; else noError">
<ion-label> <ion-label>
<ion-text color="danger"> {{ loadingError }} </ion-text> <ion-text color="danger">{{ loadingError }}</ion-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
@@ -59,13 +59,14 @@
<h2 style="display: flex; align-items: center"> <h2 style="display: flex; align-items: center">
<img <img
style="width: 18px; margin: 4px" style="width: 18px; margin: 4px"
[src]="pkg['static-files'].icon" [src]="pkg.icon"
[alt]="pkg.manifest.title" [alt]="pkg.manifest.title"
/> />
<ion-text <ion-text
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px" style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
>{{ pkg.manifest.title }}</ion-text
> >
{{ pkg.manifest.title }}
</ion-text>
</h2> </h2>
<p> <p>
<ion-text color="dark"> <ion-text color="dark">
@@ -81,7 +82,7 @@
</ion-item> </ion-item>
<!-- no options --> <!-- no options -->
<ion-item *ngIf="!hasOptions"> <ion-item *ngIf="!pkg.installed?.['has-config']">
<ion-label> <ion-label>
<p> <p>
No config options for {{ pkg.manifest.title }} {{ No config options for {{ pkg.manifest.title }} {{
@@ -112,7 +113,7 @@
<ion-toolbar> <ion-toolbar>
<ng-container *ngIf="!loading && !loadingError"> <ng-container *ngIf="!loading && !loadingError">
<ion-buttons <ion-buttons
*ngIf="configForm && hasOptions" *ngIf="configForm && pkg.installed?.['has-config']"
slot="start" slot="start"
class="ion-padding-start" class="ion-padding-start"
> >

View File

@@ -17,6 +17,7 @@ import { InputSpec } from 'start-sdk/types/config-types'
import { import {
DataModel, DataModel,
PackageDataEntry, PackageDataEntry,
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 { UntypedFormGroup } from '@angular/forms' import { UntypedFormGroup } from '@angular/forms'
@@ -36,10 +37,10 @@ import { Breakages } from 'src/app/services/api/api.types'
}) })
export class AppConfigPage { export class AppConfigPage {
@Input() pkgId!: string @Input() pkgId!: string
@Input() dependentInfo?: DependentInfo @Input() dependentInfo?: DependentInfo
pkg!: PackageDataEntry pkg!: PackageDataEntry
loadingText = '' loadingText = ''
configSpec?: InputSpec configSpec?: InputSpec
@@ -53,8 +54,6 @@ export class AppConfigPage {
saving = false saving = false
loadingError: string | IonicSafeString = '' loadingError: string | IonicSafeString = ''
hasOptions = false
constructor( constructor(
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
@@ -68,10 +67,9 @@ export class AppConfigPage {
async ngOnInit() { async ngOnInit() {
try { try {
const pkg = await getPackage(this.patch, this.pkgId) const pkg = await getPackage(this.patch, this.pkgId)
if (!pkg) return if (!pkg?.installed?.['has-config']) return
this.pkg = pkg
if (!this.pkg.manifest.config) return this.pkg = pkg
let newConfig: object | undefined let newConfig: object | undefined
let patch: Operation[] | undefined let patch: Operation[] | undefined
@@ -104,8 +102,6 @@ export class AppConfigPage {
newConfig || this.original, newConfig || this.original,
) )
this.hasOptions = false
if (patch) { if (patch) {
this.diff = this.getDiff(patch) this.diff = this.getDiff(patch)
this.markDirty(patch) this.markDirty(patch)

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular' import { ModalController } from '@ionic/angular'
import { map, take } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model' import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs' import { firstValueFrom } from 'rxjs'
@@ -36,7 +36,7 @@ export class BackupSelectPage {
return { return {
id, id,
title, title,
icon: pkg['static-files'].icon, icon: pkg.icon,
disabled: pkg.state !== PackageState.Installed, disabled: pkg.state !== PackageState.Installed,
checked: pkg.state === PackageState.Installed, checked: pkg.state === PackageState.Installed,
} }

View File

@@ -2,7 +2,11 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppActionsPage, AppActionsItemComponent } from './app-actions.page' import {
AppActionsPage,
AppActionsItemComponent,
GroupActionsPipe,
} from './app-actions.page'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
@@ -25,6 +29,6 @@ const routes: Routes = [
GenericFormPageModule, GenericFormPageModule,
ActionSuccessPageModule, ActionSuccessPageModule,
], ],
declarations: [AppActionsPage, AppActionsItemComponent], declarations: [AppActionsPage, AppActionsItemComponent, GroupActionsPipe],
}) })
export class AppActionsPageModule {} export class AppActionsPageModule {}

View File

@@ -21,17 +21,19 @@
></app-actions-item> ></app-actions-item>
<!-- ** specific actions ** --> <!-- ** specific actions ** -->
<ion-item-divider *ngIf="!(pkg.manifest.actions | empty)"> <ng-container *ngIf="pkg.actions | groupActions as actionGroups">
Actions for {{ pkg.manifest.title }} <ion-item-divider>Actions for {{ pkg.manifest.title }}</ion-item-divider>
</ion-item-divider> <div *ngFor="let group of actionGroups" class="ion-padding-bottm">
<app-actions-item <app-actions-item
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder" *ngFor="let action of group"
[action]="{ [action]="{
name: action.value.name, name: action.name,
description: action.value.description, description: action.description,
icon: 'play-circle-outline' icon: 'play-circle-outline'
}" }"
(click)="handleAction(pkg, action)" (click)="handleAction(action)"
></app-actions-item> ></app-actions-item>
</div>
</ng-container>
</ion-item-group> </ion-item-group>
</ion-content> </ion-content>

View File

@@ -1,4 +1,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import {
ChangeDetectionStrategy,
Component,
Input,
Pipe,
PipeTransform,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { import {
@@ -12,12 +18,18 @@ import {
Action, Action,
DataModel, DataModel,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' import {
isEmptyObject,
ErrorToastService,
getPkgId,
WithId,
} from '@start9labs/shared'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { filter } from 'rxjs'
@Component({ @Component({
selector: 'app-actions', selector: 'app-actions',
@@ -27,7 +39,9 @@ import { hasCurrentDeps } from 'src/app/util/has-deps'
}) })
export class AppActionsPage { export class AppActionsPage {
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
readonly pkg$ = this.patch.watch$('package-data', this.pkgId) readonly pkg$ = this.patch
.watch$('package-data', this.pkgId)
.pipe(filter(pkg => pkg.state === PackageState.Installed))
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
@@ -40,28 +54,27 @@ export class AppActionsPage {
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
async handleAction( async handleAction(action: WithId<Action>) {
pkg: PackageDataEntry, if (action.disabled) {
action: { key: string; value: Action }, const alert = await this.alertCtrl.create({
) { header: 'Forbidden',
const status = pkg.installed?.status message: action.disabled,
if ( buttons: ['OK'],
status && cssClass: 'alert-error-message enter-click',
(action.value['allowed-statuses'] as PackageMainStatus[]).includes( })
status.main.status, await alert.present()
) } else {
) { if (!isEmptyObject(action['input-spec'] || {})) {
if (!isEmptyObject(action.value['input-spec'] || {})) {
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
component: GenericFormPage, component: GenericFormPage,
componentProps: { componentProps: {
title: action.value.name, title: action.name,
spec: action.value['input-spec'], spec: action['input-spec'],
buttons: [ buttons: [
{ {
text: 'Execute', text: 'Execute',
handler: (value: any) => { handler: (value: any) => {
return this.executeAction(action.key, value) return this.executeAction(action.id, value)
}, },
isSubmit: true, isSubmit: true,
}, },
@@ -72,9 +85,9 @@ export class AppActionsPage {
} else { } else {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Confirm', header: 'Confirm',
message: `Are you sure you want to execute action "${ message: `Are you sure you want to execute action "${action.name}"? ${
action.value.name action.warning || ''
}"? ${action.value.warning || ''}`, }`,
buttons: [ buttons: [
{ {
text: 'Cancel', text: 'Cancel',
@@ -83,7 +96,7 @@ export class AppActionsPage {
{ {
text: 'Execute', text: 'Execute',
handler: () => { handler: () => {
this.executeAction(action.key) this.executeAction(action.id)
}, },
cssClass: 'enter-click', cssClass: 'enter-click',
}, },
@@ -91,31 +104,6 @@ export class AppActionsPage {
}) })
await alert.present() await alert.present()
} }
} else {
const statuses = [...action.value['allowed-statuses']]
const last = statuses.pop()
let statusesStr = statuses.join(', ')
let error = ''
if (statuses.length) {
if (statuses.length > 1) {
// oxford comma
statusesStr += ','
}
statusesStr += ` or ${last}`
} else if (last) {
statusesStr = `${last}`
} else {
error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
}
const alert = await this.alertCtrl.create({
header: 'Forbidden',
message:
error ||
`Action "${action.value.name}" can only be executed when service is ${statusesStr}`,
buttons: ['OK'],
cssClass: 'alert-error-message enter-click',
})
await alert.present()
} }
} }
@@ -224,3 +212,31 @@ interface LocalAction {
export class AppActionsItemComponent { export class AppActionsItemComponent {
@Input() action!: LocalAction @Input() action!: LocalAction
} }
@Pipe({
name: 'groupActions',
})
export class GroupActionsPipe implements PipeTransform {
transform(
actions: PackageDataEntry['actions'],
): Array<Array<WithId<Action>>> | null {
if (!actions) return null
const noGroup = 'noGroup'
const grouped = Object.entries(actions).reduce<
Record<string, WithId<Action>[]>
>((groups, [id, action]) => {
const actionWithId = { id, ...action }
const groupKey = action.group || noGroup
if (!groups[groupKey]) {
groups[groupKey] = [actionWithId]
} else {
groups[groupKey].push(actionWithId)
}
return groups
}, {})
return Object.values(grouped).map(group =>
group.sort((a, b) => a.name.localeCompare(b.name)),
)
}
}

View File

@@ -1,72 +1,34 @@
<ion-item *ngIf="interface"> <ion-item>
<ion-icon <ion-icon
slot="start" slot="start"
size="large" size="large"
[name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'" [name]="addressInfo.ui ? 'desktop-outline' : 'terminal-outline'"
></ion-icon> ></ion-icon>
<ion-label> <ion-label>
<h1>{{ interface.def.name }}</h1> <h1>{{ addressInfo.name }}</h1>
<h2>{{ interface.def.description }}</h2> <h2>{{ addressInfo.description }}</h2>
</ion-label> </ion-label>
</ion-item> </ion-item>
<div *ngIf="interface" style="padding-left: 64px"> <div style="padding-left: 64px">
<!-- has tor --> <ion-item *ngFor="let address of addressInfo.addresses">
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
<ion-label> <ion-label>
<h2>Tor Address</h2> <h2>{{ address | addressType }}</h2>
<p>{{ tor }}</p> <p>{{ address }}</p>
</ion-label> </ion-label>
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(tor)"> <ion-button *ngIf="addressInfo.ui" fill="clear" (click)="launch(address)">
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon> <ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
</ion-button> </ion-button>
<ion-button fill="clear" (click)="showQR(tor)"> <ion-button fill="clear" (click)="showQR(address)">
<ion-icon <ion-icon
size="small" size="small"
slot="icon-only" slot="icon-only"
name="qr-code-outline" name="qr-code-outline"
></ion-icon> ></ion-icon>
</ion-button> </ion-button>
<ion-button fill="clear" (click)="copy(tor)"> <ion-button fill="clear" (click)="copy(address)">
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon> <ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
</ion-item> </ion-item>
<!-- no tor -->
<ion-item *ngIf="!interface.addresses['tor-address']">
<ion-label>
<h2>Tor Address</h2>
<p>Service does not use a Tor Address</p>
</ion-label>
</ion-item>
<!-- lan -->
<ion-item *ngIf="interface.addresses['lan-address'] as lan">
<ion-label>
<h2>LAN Address</h2>
<p>{{ lan }}</p>
</ion-label>
<ion-buttons slot="end">
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(lan)">
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="showQR(lan)">
<ion-icon
size="small"
slot="icon-only"
name="qr-code-outline"
></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="copy(lan)">
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-item>
<!-- no lan -->
<ion-item *ngIf="!interface.addresses['lan-address']">
<ion-label>
<h2>LAN Address</h2>
<p>N/A</p>
</ion-label>
</ion-item>
</div> </div>

View File

@@ -3,10 +3,10 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { import {
AppInterfacesItemComponent, AppInterfacesItemComponent,
AppInterfacesPage, AppInterfacesPage,
AddressTypePipe,
} from './app-interfaces.page' } from './app-interfaces.page'
const routes: Routes = [ const routes: Routes = [
@@ -23,6 +23,10 @@ const routes: Routes = [
RouterModule.forChild(routes), RouterModule.forChild(routes),
SharedPipesModule, SharedPipesModule,
], ],
declarations: [AppInterfacesPage, AppInterfacesItemComponent], declarations: [
AppInterfacesPage,
AppInterfacesItemComponent,
AddressTypePipe,
],
}) })
export class AppInterfacesPageModule {} export class AppInterfacesPageModule {}

View File

@@ -9,18 +9,11 @@
<ion-content class="ion-padding-top with-widgets"> <ion-content class="ion-padding-top with-widgets">
<ion-item-group> <ion-item-group>
<!-- iff ui --> <div
<ng-container *ngIf="ui"> *ngFor="let addressInfo of (addressInfo$ | async)"
<ion-item-divider>User Interface</ion-item-divider> style="margin-bottom: 30px"
<app-interfaces-item [interface]="ui"></app-interfaces-item> >
</ng-container> <app-interfaces-item [addressInfo]="addressInfo"></app-interfaces-item>
</div>
<!-- other interface -->
<ng-container *ngIf="other.length">
<ion-item-divider>Machine Interfaces</ion-item-divider>
<div *ngFor="let interface of other" style="margin-bottom: 30px">
<app-interfaces-item [interface]="interface"></app-interfaces-item>
</div>
</ng-container>
</ion-item-group> </ion-item-group>
</ion-content> </ion-content>

View File

@@ -1,80 +1,38 @@
import { Component, Input } from '@angular/core' import {
ChangeDetectionStrategy,
Component,
Input,
Pipe,
PipeTransform,
} from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ModalController, ToastController } from '@ionic/angular' import { ModalController, ToastController } from '@ionic/angular'
import { getPkgId, copyToClipboard } from '@start9labs/shared' import { getPkgId, copyToClipboard } from '@start9labs/shared'
import { getUiInterfaceKey } from 'src/app/services/config.service' import { AddressInfo, DataModel } from 'src/app/services/patch-db/data-model'
import {
DataModel,
InstalledPackageDataEntry,
InterfaceDef,
} from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { QRComponent } from 'src/app/components/qr/qr.component' import { QRComponent } from 'src/app/components/qr/qr.component'
import { getPackage } from '../../../util/get-package-data' import { map } from 'rxjs'
interface LocalInterface {
def: InterfaceDef
addresses: InstalledPackageDataEntry['interface-addresses'][string]
}
@Component({ @Component({
selector: 'app-interfaces', selector: 'app-interfaces',
templateUrl: './app-interfaces.page.html', templateUrl: './app-interfaces.page.html',
styleUrls: ['./app-interfaces.page.scss'], styleUrls: ['./app-interfaces.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AppInterfacesPage { export class AppInterfacesPage {
ui?: LocalInterface
other: LocalInterface[] = []
readonly pkgId = getPkgId(this.route) readonly pkgId = getPkgId(this.route)
readonly addressInfo$ = this.patch
.watch$('package-data', this.pkgId, 'installed', 'address-info')
.pipe(
map(addressInfo =>
Object.values(addressInfo).sort((a, b) => a.name.localeCompare(b.name)),
),
)
constructor( constructor(
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
async ngOnInit() {
const pkg = await getPackage(this.patch, this.pkgId)
if (!pkg) return
const interfaces = pkg.manifest.interfaces
const uiKey = getUiInterfaceKey(interfaces)
if (!pkg.installed) return
const addressesMap = pkg.installed['interface-addresses']
if (uiKey) {
const uiAddresses = addressesMap[uiKey]
this.ui = {
def: interfaces[uiKey],
addresses: {
'lan-address': uiAddresses['lan-address']
? 'https://' + uiAddresses['lan-address']
: '',
'tor-address': uiAddresses['tor-address']
? 'http://' + uiAddresses['tor-address']
: '',
},
}
}
this.other = Object.keys(interfaces)
.filter(key => key !== uiKey)
.map(key => {
const addresses = addressesMap[key]
return {
def: interfaces[key],
addresses: {
'lan-address': addresses['lan-address']
? 'https://' + addresses['lan-address']
: '',
'tor-address': addresses['tor-address']
? 'http://' + addresses['tor-address']
: '',
},
}
})
}
} }
@Component({ @Component({
@@ -84,7 +42,7 @@ export class AppInterfacesPage {
}) })
export class AppInterfacesItemComponent { export class AppInterfacesItemComponent {
@Input() @Input()
interface!: LocalInterface addressInfo!: AddressInfo
constructor( constructor(
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
@@ -122,3 +80,31 @@ export class AppInterfacesItemComponent {
await toast.present() await toast.present()
} }
} }
@Pipe({
name: 'addressType',
})
export class AddressTypePipe implements PipeTransform {
transform(address: string): string {
if (isValidIpv4(address)) return 'IPv4'
if (isValidIpv6(address)) return 'IPv6'
const hostname = new URL(address).hostname
if (hostname.endsWith('.onion')) return 'Tor'
if (hostname.endsWith('.local')) return 'Local'
return 'Custom'
}
}
function isValidIpv4(address: string): boolean {
const regexExp =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
return regexExp.test(address)
}
function isValidIpv6(address: string): boolean {
const regexExp =
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi
return regexExp.test(address)
}

View File

@@ -7,7 +7,7 @@
> >
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon> <app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
<ion-thumbnail slot="start"> <ion-thumbnail slot="start">
<img alt="" [src]="pkg.entry['static-files'].icon" /> <img alt="" [src]="pkg.entry.icon" />
</ion-thumbnail> </ion-thumbnail>
<ion-label> <ion-label>
<h2 ticker>{{ manifest.title }}</h2> <h2 ticker>{{ manifest.title }}</h2>
@@ -17,17 +17,18 @@
[installProgress]="pkg.entry['install-progress']" [installProgress]="pkg.entry['install-progress']"
weight="bold" weight="bold"
size="small" size="small"
[sigtermTimeout]="manifest.main['sigterm-timeout']"
></status> ></status>
</ion-label> </ion-label>
<ion-button <ng-container *ngIf="pkg.entry.installed as installed">
*ngIf="manifest.interfaces | hasUi" <ion-button
slot="end" *ngIf="installed['address-info'] | hasUi"
fill="clear" slot="end"
color="primary" fill="clear"
(click)="launchUi($event)" color="primary"
[disabled]="!(pkg.entry.state | isLaunchable: status:manifest.interfaces)" (click)="launchUi($event, installed['address-info'])"
> [disabled]="status !== 'running'"
<ion-icon slot="icon-only" name="open-outline"></ion-icon> >
</ion-button> <ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
</ng-container>
</ion-item> </ion-item>

View File

@@ -1,5 +1,8 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { PackageMainStatus } from 'src/app/services/patch-db/data-model' import {
InstalledPackageInfo,
PackageMainStatus,
} from 'src/app/services/patch-db/data-model'
import { PkgInfo } from 'src/app/util/get-package-info' import { PkgInfo } from 'src/app/util/get-package-info'
import { UiLauncherService } from 'src/app/services/ui-launcher.service' import { UiLauncherService } from 'src/app/services/ui-launcher.service'
@@ -20,9 +23,9 @@ export class AppListPkgComponent {
) )
} }
launchUi(e: Event): void { launchUi(e: Event, addressInfo: InstalledPackageInfo['address-info']): void {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
this.launcherService.launch(this.pkg.entry) this.launcherService.launch(addressInfo)
} }
} }

View File

@@ -11,7 +11,6 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module' import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
import { AppListIconComponent } from './app-list-icon/app-list-icon.component' import { AppListIconComponent } from './app-list-icon/app-list-icon.component'
import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component' import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component'
@@ -31,7 +30,6 @@ const routes: Routes = [
StatusComponentModule, StatusComponentModule,
EmverPipesModule, EmverPipesModule,
TextSpinnerComponentModule, TextSpinnerComponentModule,
LaunchablePipeModule,
UiPipeModule, UiPipeModule,
IonicModule, IonicModule,
RouterModule.forChild(routes), RouterModule.forChild(routes),

View File

@@ -3,10 +3,13 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { AppShowPage } from './app-show.page' import { AppShowPage } from './app-show.page'
import { EmverPipesModule, ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' import {
EmverPipesModule,
ResponsiveColModule,
SharedPipesModule,
} from '@start9labs/shared'
import { StatusComponentModule } from 'src/app/components/status/status.component.module' import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module' import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module' import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component' import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component'
import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component' import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component'
@@ -51,7 +54,6 @@ const routes: Routes = [
RouterModule.forChild(routes), RouterModule.forChild(routes),
AppConfigPageModule, AppConfigPageModule,
EmverPipesModule, EmverPipesModule,
LaunchablePipeModule,
UiPipeModule, UiPipeModule,
ResponsiveColModule, ResponsiveColModule,
SharedPipesModule, SharedPipesModule,

View File

@@ -44,7 +44,7 @@
detail="false" detail="false"
> >
<ion-label> <ion-label>
<h2>Marketing Site</h2> <h2>Website</h2>
<p>{{ manifest['marketing-site'] || 'Not provided' }}</p> <p>{{ manifest['marketing-site'] || 'Not provided' }}</p>
</ion-label> </ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon> <ion-icon slot="end" name="open-outline"></ion-icon>

View File

@@ -35,10 +35,16 @@ export class AppShowAdditionalComponent {
} }
async presentModalLicense() { async presentModalLicense() {
const { id, version } = this.pkg.manifest
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
componentProps: { componentProps: {
title: 'License', title: 'License',
content: from(this.api.getStatic(this.pkg['static-files']['license'])), content: from(
this.api.getStatic(
`/public/package-data/${id}/${version}/LICENSE.md`,
),
),
}, },
component: MarkdownComponent, component: MarkdownComponent,
}) })

View File

@@ -4,7 +4,7 @@
<ion-back-button defaultHref="services"></ion-back-button> <ion-back-button defaultHref="services"></ion-back-button>
</ion-buttons> </ion-buttons>
<div class="header"> <div class="header">
<img class="logo" [src]="pkg['static-files'].icon" alt="" /> <img class="logo" [src]="pkg.icon" alt="" />
<ion-label> <ion-label>
<h1 <h1
class="montserrat" class="montserrat"

View File

@@ -6,7 +6,6 @@
weight="600" weight="600"
[installProgress]="pkg['install-progress']" [installProgress]="pkg['install-progress']"
[rendering]="PR[status.primary]" [rendering]="PR[status.primary]"
[sigtermTimeout]="pkg.manifest.main['sigterm-timeout']"
></status> ></status>
</ion-label> </ion-label>
</ion-item> </ion-item>
@@ -31,7 +30,7 @@
</ng-container> </ng-container>
<ion-button <ion-button
*ngIf="isStopped && pkgStatus?.configured" *ngIf="isStopped && isConfigured"
class="action-button" class="action-button"
color="success" color="success"
(click)="tryStart()" (click)="tryStart()"
@@ -41,7 +40,7 @@
</ion-button> </ion-button>
<ion-button <ion-button
*ngIf="!pkgStatus?.configured" *ngIf="!isConfigured"
class="action-button" class="action-button"
color="warning" color="warning"
(click)="presentModalConfig()" (click)="presentModalConfig()"
@@ -51,16 +50,14 @@
</ion-button> </ion-button>
<ion-button <ion-button
*ngIf="pkgStatus && (interfaces | hasUi)" *ngIf="addressInfo | hasUi"
class="action-button" class="action-button"
color="primary" color="primary"
[disabled]=" [disabled]="status.primary === 'running'"
!(pkg.state | isLaunchable: pkgStatus.main.status:interfaces) (click)="launchUi(addressInfo)"
"
(click)="launchUi()"
> >
<ion-icon slot="start" name="open-outline"></ion-icon> <ion-icon slot="start" name="open-outline"></ion-icon>
Launch UI Open UI
</ion-button> </ion-button>
</ion-col> </ion-col>
</ion-row> </ion-row>

View File

@@ -6,11 +6,11 @@ import {
PrimaryStatus, PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service' } from 'src/app/services/pkg-status-rendering.service'
import { import {
AddressInfo,
DataModel, DataModel,
InterfaceDef, InstalledPackageInfo,
PackageDataEntry, PackageDataEntry,
PackageState, PackageState,
Status,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import { AlertController, LoadingController } from '@ionic/angular' import { AlertController, LoadingController } from '@ionic/angular'
@@ -56,12 +56,12 @@ export class AppShowStatusComponent {
return this.pkg.manifest.id return this.pkg.manifest.id
} }
get interfaces(): Record<string, InterfaceDef> { get addressInfo(): Record<string, AddressInfo> {
return this.pkg.manifest.interfaces || {} return this.pkg.installed!['address-info']
} }
get pkgStatus(): Status | null { get isConfigured(): boolean {
return this.pkg.installed?.status || null return this.pkg.installed!.status.configured
} }
get isInstalled(): boolean { get isInstalled(): boolean {
@@ -76,8 +76,8 @@ export class AppShowStatusComponent {
return this.status.primary === PrimaryStatus.Stopped return this.status.primary === PrimaryStatus.Stopped
} }
launchUi(): void { launchUi(addressInfo: InstalledPackageInfo['address-info']): void {
this.launcherService.launch(this.pkg) this.launcherService.launch(addressInfo)
} }
async presentModalConfig(): Promise<void> { async presentModalConfig(): Promise<void> {

View File

@@ -98,15 +98,19 @@ export class ToButtonsPipe implements PipeTransform {
} }
private async presentModalInstructions(pkg: PackageDataEntry) { private async presentModalInstructions(pkg: PackageDataEntry) {
const { id, version } = pkg.manifest
this.apiService this.apiService
.setDbValue<boolean>(['ack-instructions', pkg.manifest.id], true) .setDbValue<boolean>(['ack-instructions', id], 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({
componentProps: { componentProps: {
title: 'Instructions', title: 'Instructions',
content: from( content: from(
this.apiService.getStatic(pkg['static-files']['instructions']), this.apiService.getStatic(
`/public/package-data/${id}/${version}/INSTRUCTIONS.md`,
),
), ),
}, },
component: MarkdownComponent, component: MarkdownComponent,

View File

@@ -3,11 +3,11 @@ import { NavigationExtras } from '@angular/router'
import { NavController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { import {
DependencyErrorType, DependencyErrorType,
InstalledPackageDataEntry,
PackageDataEntry, PackageDataEntry,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { DependentInfo } from 'src/app/types/dependent-info' import { DependentInfo } from 'src/app/types/dependent-info'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'
import { Manifest } from '@start9labs/marketplace'
export interface DependencyInfo { export interface DependencyInfo {
id: string id: string
@@ -32,40 +32,32 @@ export class ToDependenciesPipe implements PipeTransform {
if (!pkg.installed) return [] if (!pkg.installed) return []
return Object.keys(pkg.installed['current-dependencies']) return Object.keys(pkg.installed['current-dependencies'])
.filter(id => !!pkg.manifest.dependencies[id]) .filter(depId => !!pkg.manifest.dependencies[depId])
.map(id => this.setDepValues(pkg.installed!, id)) .map(depId => this.setDepValues(pkg, depId))
} }
private setDepValues( private setDepValues(pkg: PackageDataEntry, depId: string): DependencyInfo {
pkg: InstalledPackageDataEntry,
id: string,
): DependencyInfo {
let errorText = '' let errorText = ''
let actionText = 'View' let actionText = 'View'
let action: () => any = () => let action: () => any = () =>
this.navCtrl.navigateForward(`/services/${id}`) this.navCtrl.navigateForward(`/services/${depId}`)
const error = pkg.status['dependency-errors'][id] const error = pkg.installed!.status['dependency-errors'][depId]
if (error) { if (error) {
// health checks failed // health checks failed
if ( if (error.type === DependencyErrorType.HealthChecksFailed) {
[
DependencyErrorType.InterfaceHealthChecksFailed,
DependencyErrorType.HealthChecksFailed,
].includes(error.type)
) {
errorText = 'Health check failed' errorText = 'Health check failed'
// not installed // not installed
} else if (error.type === DependencyErrorType.NotInstalled) { } else if (error.type === DependencyErrorType.NotInstalled) {
errorText = 'Not installed' errorText = 'Not installed'
actionText = 'Install' actionText = 'Install'
action = () => this.fixDep(pkg, 'install', id) action = () => this.fixDep(pkg, 'install', depId)
// incorrect version // incorrect version
} else if (error.type === DependencyErrorType.IncorrectVersion) { } else if (error.type === DependencyErrorType.IncorrectVersion) {
errorText = 'Incorrect version' errorText = 'Incorrect version'
actionText = 'Update' actionText = 'Update'
action = () => this.fixDep(pkg, 'update', id) action = () => this.fixDep(pkg, 'update', depId)
// not running // not running
} else if (error.type === DependencyErrorType.NotRunning) { } else if (error.type === DependencyErrorType.NotRunning) {
errorText = 'Not running' errorText = 'Not running'
@@ -74,19 +66,19 @@ export class ToDependenciesPipe implements PipeTransform {
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) { } else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config not satisfied' errorText = 'Config not satisfied'
actionText = 'Auto config' actionText = 'Auto config'
action = () => this.fixDep(pkg, 'configure', id) action = () => this.fixDep(pkg, 'configure', depId)
} else if (error.type === DependencyErrorType.Transitive) { } else if (error.type === DependencyErrorType.Transitive) {
errorText = 'Dependency has a dependency issue' errorText = 'Dependency has a dependency issue'
} }
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.` errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
} }
const depInfo = pkg['dependency-info'][id] const depInfo = pkg.installed!['dependency-info'][depId]
return { return {
id, id: depId,
version: pkg.manifest.dependencies[id].version, version: pkg.manifest.dependencies[depId].version,
title: depInfo?.manifest?.title || id, title: depInfo?.title || depId,
icon: depInfo?.icon || '', icon: depInfo?.icon || '',
errorText, errorText,
actionText, actionText,
@@ -95,28 +87,25 @@ export class ToDependenciesPipe implements PipeTransform {
} }
async fixDep( async fixDep(
pkg: InstalledPackageDataEntry, pkg: PackageDataEntry,
action: 'install' | 'update' | 'configure', action: 'install' | 'update' | 'configure',
id: string, depId: string,
): Promise<void> { ): Promise<void> {
switch (action) { switch (action) {
case 'install': case 'install':
case 'update': case 'update':
return this.installDep(pkg, id) return this.installDep(pkg.manifest, depId)
case 'configure': case 'configure':
return this.configureDep(pkg, id) return this.configureDep(pkg.manifest, depId)
} }
} }
private async installDep( private async installDep(manifest: Manifest, depId: string): Promise<void> {
pkg: InstalledPackageDataEntry, const version = manifest.dependencies[depId].version
depId: string,
): Promise<void> {
const version = pkg.manifest.dependencies[depId].version
const dependentInfo: DependentInfo = { const dependentInfo: DependentInfo = {
id: pkg.manifest.id, id: manifest.id,
title: pkg.manifest.title, title: manifest.title,
version, version,
} }
const navigationExtras: NavigationExtras = { const navigationExtras: NavigationExtras = {
@@ -130,12 +119,12 @@ export class ToDependenciesPipe implements PipeTransform {
} }
private async configureDep( private async configureDep(
pkg: InstalledPackageDataEntry, manifest: Manifest,
dependencyId: string, dependencyId: string,
): Promise<void> { ): Promise<void> {
const dependentInfo: DependentInfo = { const dependentInfo: DependentInfo = {
id: pkg.manifest.id, id: manifest.id,
title: pkg.manifest.title, title: manifest.title,
} }
await this.modalService.presentModalConfig({ await this.modalService.presentModalConfig({

View File

@@ -180,7 +180,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
}, },
'marketing-site': { 'marketing-site': {
type: 'string', type: 'string',
name: 'Marketing Site', name: 'Website',
description: 'URL to the marketing site / channel for the project', description: 'URL to the marketing site / channel for the project',
placeholder: 'e.g. start9.com', placeholder: 'e.g. start9.com',
pattern: null, pattern: null,

View File

@@ -0,0 +1,46 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import {
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
} from '@start9labs/shared'
import {
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
} from '@start9labs/marketplace'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
@NgModule({
declarations: [
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
],
imports: [
CommonModule,
IonicModule,
RouterModule,
TextSpinnerComponentModule,
SharedPipesModule,
EmverPipesModule,
MarkdownPipeModule,
PackageModule,
AboutModule,
DependenciesModule,
AdditionalModule,
],
exports: [
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
],
})
export class MarketplaceShowComponentsModule {}

View File

@@ -10,7 +10,7 @@
<ng-container *ngIf="localPkg; else install"> <ng-container *ngIf="localPkg; else install">
<ng-container *ngIf="localPkg.state === PackageState.Installed"> <ng-container *ngIf="localPkg.state === PackageState.Installed">
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1" *ngIf="(localVersion | compareEmver : pkg.manifest.version) === -1"
expand="block" expand="block"
color="success" color="success"
(click)="tryInstall()" (click)="tryInstall()"
@@ -18,7 +18,7 @@
Update Update
</ion-button> </ion-button>
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 1" *ngIf="(localVersion | compareEmver : pkg.manifest.version) === 1"
expand="block" expand="block"
color="warning" color="warning"
(click)="tryInstall()" (click)="tryInstall()"
@@ -27,7 +27,7 @@
</ion-button> </ion-button>
<ng-container *ngIf="showDevTools$ | async"> <ng-container *ngIf="showDevTools$ | async">
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 0" *ngIf="(localVersion | compareEmver : pkg.manifest.version) === 0"
expand="block" expand="block"
color="success" color="success"
(click)="tryInstall()" (click)="tryInstall()"

View File

@@ -13,13 +13,13 @@
<br /> <br />
<br /> <br />
<span <span
*ngIf="version | satisfiesEmver: dependentInfo.version" *ngIf="version | satisfiesEmver : dependentInfo.version"
class="text" class="text"
> >
{{ title }} version {{ version | displayEmver }} is compatible. {{ title }} version {{ version | displayEmver }} is compatible.
</span> </span>
<span <span
*ngIf="!(version | satisfiesEmver: dependentInfo.version)" *ngIf="!(version | satisfiesEmver : dependentInfo.version)"
class="text text_error" class="text text_error"
> >
{{ title }} version {{ version | displayEmver }} is NOT compatible. {{ title }} version {{ version | displayEmver }} is NOT compatible.

View File

@@ -16,9 +16,7 @@ import {
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceShowPage } from './marketplace-show.page' import { MarketplaceShowPage } from './marketplace-show.page'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component' import { MarketplaceShowComponentsModule } from './components/marketplace-show-components.module'
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -41,18 +39,8 @@ const routes: Routes = [
AboutModule, AboutModule,
DependenciesModule, DependenciesModule,
AdditionalModule, AdditionalModule,
MarketplaceShowComponentsModule,
], ],
declarations: [ declarations: [MarketplaceShowPage],
MarketplaceShowPage,
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
],
exports: [
MarketplaceShowPage,
MarketplaceShowHeaderComponent,
MarketplaceShowControlsComponent,
MarketplaceShowDependentComponent,
],
}) })
export class MarketplaceShowPageModule {} export class MarketplaceShowPageModule {}

View File

@@ -15,9 +15,9 @@
<ng-container *ngFor="let pkg of pkgs | keyvalue"> <ng-container *ngFor="let pkg of pkgs | keyvalue">
<ion-item *ngIf="backupProgress[pkg.key] as pkgProgress"> <ion-item *ngIf="backupProgress[pkg.key] as pkgProgress">
<ion-avatar slot="start"> <ion-avatar slot="start">
<img [src]="pkg.value['static-files'].icon" /> <img [src]="pkg.value.icon" />
</ion-avatar> </ion-avatar>
<ion-label> {{ pkg.value.manifest.title }} </ion-label> <ion-label>{{ pkg.value.manifest.title }}</ion-label>
<!-- complete --> <!-- complete -->
<ion-note <ion-note
*ngIf="pkgProgress.complete; else incomplete" *ngIf="pkgProgress.complete; else incomplete"

View File

@@ -5,6 +5,13 @@ import { SideloadPage } from './sideload.page'
import { Routes, RouterModule } from '@angular/router' import { Routes, RouterModule } from '@angular/router'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared' import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { DragNDropDirective } from './dnd.directive' import { DragNDropDirective } from './dnd.directive'
import {
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
} from '@start9labs/marketplace'
import { MarketplaceShowComponentsModule } from '../../marketplace-routes/marketplace-show/components/marketplace-show-components.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -20,6 +27,11 @@ const routes: Routes = [
RouterModule.forChild(routes), RouterModule.forChild(routes),
SharedPipesModule, SharedPipesModule,
EmverPipesModule, EmverPipesModule,
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
MarketplaceShowComponentsModule,
], ],
declarations: [SideloadPage, DragNDropDirective], declarations: [SideloadPage, DragNDropDirective],
}) })

View File

@@ -7,92 +7,70 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-text-center with-widgets"> <ion-content class="ion-padding with-widgets">
<!-- file upload --> <!-- invalid -->
<div <div *ngIf="invalid; else valid" class="drop-area_filled">
*ngIf="!toUpload.file; else fileUploaded" <h4>
class="drop-area" <ion-icon
[class.drop-area_mobile]="isMobile" name="close-circle-outline"
appDnd color="danger"
(onFileDropped)="handleFileDrop($event)" class="inline"
> ></ion-icon>
<ion-icon Invalid package file
name="cloud-upload-outline" </h4>
color="dark" <ion-button color="primary" (click)="clear()">Try again</ion-button>
style="font-size: 42px"
></ion-icon>
<h4>Upload .s9pk package file</h4>
<p *ngIf="onTor">
<ion-text color="success"
>Tip: switch to LAN for faster uploads.</ion-text
>
</p>
<ion-button color="primary" type="file" class="ion-margin-top">
<label for="upload-photo">Browse</label>
<input
type="file"
style="position: absolute; opacity: 0; height: 100%"
id="upload-photo"
(change)="handleFileInput($event)"
/>
</ion-button>
</div> </div>
<!-- file uploaded -->
<ng-template #fileUploaded> <!-- valid -->
<div class="drop-area_filled"> <ng-template #valid>
<h4> <!-- uploaded -->
<ion-icon <div class="ion-padding" *ngIf="pkgData?.pkg as pkg; else empty">
*ngIf="uploadState?.invalid" <div class="ion-text-right">
name="close-circle-outline" <ion-button color="danger" (click)="clear()">
color="danger" <ion-icon slot="icon-only" name="close"></ion-icon>
class="inline"
></ion-icon>
<ion-icon
*ngIf="!uploadState?.invalid"
class="inline"
name="checkmark-circle-outline"
color="success"
></ion-icon>
{{ uploadState?.message }}
</h4>
<div class="box" *ngIf="toUpload.icon && toUpload.manifest">
<div class="card">
<div class="row row_end">
<ion-button
style="
--background-hover: transparent;
--padding-end: 0px;
--padding-start: 0px;
"
fill="clear"
size="small"
(click)="clearToUpload()"
>
<ion-icon slot="icon-only" name="close" color="danger"></ion-icon>
</ion-button>
</div>
<div class="row">
<img
[alt]="toUpload.manifest.title + ' Icon'"
[src]="toUpload.icon | trustUrl"
/>
<h2>{{ toUpload.manifest.title }}</h2>
<p>{{ toUpload.manifest.version | displayEmver }}</p>
</div>
</div>
</div>
<ion-button
*ngIf="!toUpload.icon && !toUpload.manifest; else uploadButton"
color="primary"
(click)="clearToUpload()"
>
Try again
</ion-button>
<ng-template #uploadButton>
<ion-button color="primary" (click)="handleUpload()">
Upload & Install
</ion-button> </ion-button>
</ng-template> </div>
<marketplace-package [pkg]="pkg"></marketplace-package>
<marketplace-show-controls [pkg]="pkg"></marketplace-show-controls>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
<ion-item-group>
<marketplace-about [pkg]="pkg"></marketplace-about>
<marketplace-dependencies
*ngIf="!(pkg.manifest.dependencies | empty)"
[pkg]="pkg"
></marketplace-dependencies>
</ion-item-group>
<marketplace-additional [pkg]="pkg"></marketplace-additional>
</div> </div>
<!-- empty -->
<ng-template #empty>
<div
class="drop-area"
[class.drop-area_mobile]="isMobile"
appDnd
(onFileDropped)="handleFileDrop($event)"
>
<ion-icon
name="cloud-upload-outline"
color="dark"
style="font-size: 42px"
></ion-icon>
<h4>Upload .s9pk package file</h4>
<p *ngIf="onTor">
<ion-text color="success">
Tip: switch to LAN for faster uploads.
</ion-text>
</p>
<ion-button color="primary" type="file" class="ion-margin-top">
<label for="upload-photo">Browse</label>
<input
type="file"
style="position: absolute; opacity: 0; height: 100%"
id="upload-photo"
(change)="handleFileInput($event)"
/>
</ion-button>
</div>
</ng-template>
</ng-template> </ng-template>
</ion-content> </ion-content>

View File

@@ -2,11 +2,6 @@
vertical-align: initial; vertical-align: initial;
} }
.area {
flex-direction: column;
justify-content: center;
}
.drop-area { .drop-area {
display: flex; display: flex;
background-color: rgba(24, 24, 24, 0.5); background-color: rgba(24, 24, 24, 0.5);
@@ -18,7 +13,7 @@
border-color: var(--ion-color-dark); border-color: var(--ion-color-dark);
color: var(--ion-color-dark); color: var(--ion-color-dark);
border-radius: 5px; border-radius: 5px;
margin: 60px; margin: 20px;
padding: 30px; padding: 30px;
min-height: 600px; min-height: 600px;
@@ -47,45 +42,3 @@
color: var(--ion-color-dark); color: var(--ion-color-dark);
} }
} }
.box {
display: flex;
justify-content: space-evenly
}
.card {
background: radial-gradient(var(--ion-color-step-100), transparent);
min-width: 200px;
max-width: 200px;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
margin: 20px 20px 40px 20px;
border-style: solid;
border-color: var(--ion-color-step-100);
border-radius: 7px;
padding: 4px 8px 8px 8px;
.row {
width: auto;
&_end {
align-self: end;
ion-button {
width: 80%;
}
}
}
img {
width: 60px;
text-align: center;
}
h2,
p {
text-align: center;
}
}

View File

@@ -1,10 +1,10 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { isPlatform, LoadingController, NavController } from '@ionic/angular' import { isPlatform, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Manifest } from 'src/app/services/patch-db/data-model' import { Manifest, MarketplacePkg } from '@start9labs/marketplace'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import cbor from 'cbor'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import cbor from 'cbor'
interface Positions { interface Positions {
[key: string]: [bigint, bigint] // [position, length] [key: string]: [bigint, bigint] // [position, length]
@@ -20,20 +20,12 @@ const VERSION = new Uint8Array([1])
}) })
export class SideloadPage { export class SideloadPage {
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android') isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
toUpload: { pkgData?: {
manifest: Manifest | null pkg: MarketplacePkg
icon: string | null file: File
file: File | null
} = {
manifest: null,
icon: null,
file: null,
} }
onTor = this.config.isTor() onTor = this.config.isTor()
uploadState?: { invalid = false
invalid: boolean
message: string
}
constructor( constructor(
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
@@ -53,63 +45,60 @@ export class SideloadPage {
this.setFile(files) this.setFile(files)
} }
async setFile(files?: File[]) { clear() {
if (!files || !files.length) return this.pkgData = undefined
const file = files[0] this.invalid = false
if (!file) return
this.toUpload.file = file
this.uploadState = await this.validateS9pk(file)
}
async validateS9pk(file: File) {
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
if (compare(magic, MAGIC) && compare(version, VERSION)) {
await this.parseS9pk(file)
return {
invalid: false,
message: 'A valid package file has been detected!',
}
} else {
return {
invalid: true,
message: 'Invalid package file',
}
}
}
clearToUpload() {
this.toUpload.file = null
this.toUpload.manifest = null
this.toUpload.icon = null
} }
async handleUpload() { async handleUpload() {
if (!this.pkgData) return
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Uploading package', message: 'Uploading package',
cssClass: 'loader', cssClass: 'loader',
}) })
await loader.present() await loader.present()
const { pkg, file } = this.pkgData
try { try {
const guid = await this.api.sideloadPackage({ const guid = await this.api.sideloadPackage({
manifest: this.toUpload.manifest!, manifest: pkg.manifest,
icon: this.toUpload.icon!, icon: pkg.icon,
size: this.toUpload.file!.size, size: file.size,
}) })
this.api this.api.uploadPackage(guid, file).catch(e => console.error(e))
.uploadPackage(guid, this.toUpload.file!)
.catch(e => console.error(e))
this.navCtrl.navigateRoot('/services') this.navCtrl.navigateRoot('/services')
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
loader.dismiss() loader.dismiss()
this.clearToUpload() this.clear()
} }
} }
async parseS9pk(file: File) { private async setFile(files?: File[]) {
if (!files || !files.length) return
const file = files[0]
if (!file) return
await this.validateS9pk(file)
}
private async validateS9pk(file: File) {
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
if (compare(magic, MAGIC) && compare(version, VERSION)) {
this.pkgData = {
pkg: await this.parseS9pk(file),
file,
}
} else {
this.invalid = true
}
}
private async parseS9pk(file: File): Promise<MarketplacePkg> {
const positions: Positions = {} const positions: Positions = {}
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point // magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
let start = 103 let start = 103
@@ -119,30 +108,51 @@ export class SideloadPage {
).getUint32(0, false) ).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any) await getPositions(start, end, file, positions, tocLength as any)
await this.getManifest(positions, file) const manifest = await this.getAsset(positions, file, 'manifest')
await this.getIcon(positions, file) const [icon] = await Promise.all([
this.getIcon(positions, file, manifest),
// this.getAsset(positions, file, 'license'),
// this.getAsset(positions, file, 'instructions'),
])
return {
manifest,
icon,
license: '',
instructions: '',
categories: [],
versions: [],
'dependency-metadata': {},
'published-at': '',
}
} }
async getManifest(positions: Positions, file: Blob) { private async getAsset(
positions: Positions,
file: Blob,
asset: 'manifest' | 'license' | 'instructions',
): Promise<any> {
const data = await blobToBuffer( const data = await blobToBuffer(
file.slice( file.slice(
Number(positions['manifest'][0]), Number(positions[asset][0]),
Number(positions['manifest'][0]) + Number(positions['manifest'][1]), Number(positions[asset][0]) + Number(positions[asset][1]),
), ),
) )
this.toUpload.manifest = await cbor.decode(data, true) return cbor.decode(data, true)
} }
async getIcon(positions: Positions, file: Blob) { private async getIcon(
const contentType = `image/${this.toUpload.manifest?.assets.icon positions: Positions,
.split('.') file: Blob,
.pop()}` manifest: Manifest,
): Promise<string> {
const contentType = `image/${manifest.assets.icon.split('.').pop()}`
const data = file.slice( const data = file.slice(
Number(positions['icon'][0]), Number(positions['icon'][0]),
Number(positions['icon'][0]) + Number(positions['icon'][1]), Number(positions['icon'][0]) + Number(positions['icon'][1]),
contentType, contentType,
) )
this.toUpload.icon = await blobToDataURL(data) return blobToDataURL(data)
} }
} }

View File

@@ -9,7 +9,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
import { import {
AbstractMarketplaceService, AbstractMarketplaceService,
Marketplace, Marketplace,
MarketplaceManifest, Manifest,
MarketplacePkg, MarketplacePkg,
StoreIdentity, StoreIdentity,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
@@ -64,7 +64,7 @@ export class UpdatesPage {
} }
async tryUpdate( async tryUpdate(
manifest: MarketplaceManifest, manifest: Manifest,
url: string, url: string,
local: PackageDataEntry, local: PackageDataEntry,
e: Event, e: Event,
@@ -83,7 +83,7 @@ export class UpdatesPage {
} }
} }
private async dryUpdate(manifest: MarketplaceManifest, url: string) { private async dryUpdate(manifest: Manifest, url: string) {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Checking dependent services...', message: 'Checking dependent services...',
}) })
@@ -181,7 +181,7 @@ export class FilterUpdatesPipe implements PipeTransform {
({ manifest }) => ({ manifest }) =>
this.emver.compare( this.emver.compare(
manifest.version, manifest.version,
local[manifest.id]?.installed?.manifest.version || '', local[manifest.id]?.manifest.version || '', // @TODO this won't work, need old version
) === 1, ) === 1,
) )
} }

View File

@@ -1,8 +0,0 @@
import { NgModule } from '@angular/core'
import { LaunchablePipe } from './launchable.pipe'
@NgModule({
declarations: [LaunchablePipe],
exports: [LaunchablePipe],
})
export class LaunchablePipeModule {}

View File

@@ -1,22 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core'
import {
InterfaceDef,
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { ConfigService } from '../../services/config.service'
@Pipe({
name: 'isLaunchable',
})
export class LaunchablePipe implements PipeTransform {
constructor(private configService: ConfigService) {}
transform(
state: PackageState,
status: PackageMainStatus,
interfaces: Record<string, InterfaceDef>,
): boolean {
return this.configService.isLaunchable(state, status, interfaces)
}
}

View File

@@ -1,12 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { InterfaceDef } from '../../services/patch-db/data-model' import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
import { hasUi } from '../../services/config.service' import { hasUi } from '../../services/config.service'
@Pipe({ @Pipe({
name: 'hasUi', name: 'hasUi',
}) })
export class UiPipe implements PipeTransform { export class UiPipe implements PipeTransform {
transform(interfaces: Record<string, InterfaceDef>): boolean { transform(addressInfo: InstalledPackageInfo['address-info']): boolean {
return hasUi(interfaces) return hasUi(addressInfo)
} }
} }

View File

@@ -1,7 +1,5 @@
import { import {
DependencyErrorType, DependencyErrorType,
DockerIoFormat,
Manifest,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
@@ -9,7 +7,11 @@ import {
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types' import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace' import {
DependencyMetadata,
MarketplacePkg,
Manifest,
} from '@start9labs/marketplace'
import { Log } from '@start9labs/shared' import { Log } from '@start9labs/shared'
export module Mock { export module Mock {
@@ -17,6 +19,7 @@ export module Mock {
'backup-progress': null, 'backup-progress': null,
'update-progress': null, 'update-progress': null,
updated: true, updated: true,
'shutting-down': false,
} }
export const MarketplaceEos: RR.GetMarketplaceEosRes = { export const MarketplaceEos: RR.GetMarketplaceEosRes = {
version: '0.3.4.3', version: '0.3.4.3',
@@ -50,16 +53,11 @@ export module Mock {
short: 'A Bitcoin full node by Bitcoin Core.', short: 'A Bitcoin full node by Bitcoin Core.',
long: 'Bitcoin is a decentralized consensus protocol and settlement network.', long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
}, },
replaces: ['banks', 'governments'],
'release-notes': 'Taproot, Schnorr, and more.',
assets: { assets: {
icon: 'icon.png', icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
}, },
replaces: ['banks', 'governments'],
'release-notes': 'Taproot, Schnorr, and more.',
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'upstream-repo': 'https://github.com/bitcoin/bitcoin', 'upstream-repo': 'https://github.com/bitcoin/bitcoin',
@@ -74,299 +72,8 @@ export module Mock {
start: 'Starting Bitcoin is good for your health.', start: 'Starting Bitcoin is good for your health.',
stop: null, stop: null,
}, },
main: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': '1ms',
},
config: {
get: null,
set: null,
},
volumes: {},
'min-os-version': '0.2.12',
interfaces: {
ui: {
name: 'Node Visualizer',
description:
'Web application for viewing information about your node and the Bitcoin network.',
ui: true,
'tor-config': {
'port-mapping': {},
},
'lan-config': {},
protocols: [],
},
rpc: {
name: 'RPC',
description: 'Used by wallets to interact with your Bitcoin Core node.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {},
protocols: [],
},
p2p: {
name: 'P2P',
description:
'Used by other Bitcoin nodes to communicate and interact with your node.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {},
protocols: [],
},
},
backup: {
create: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
restore: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
},
migrations: null,
actions: {
resync: {
name: 'Resync Blockchain',
description: 'Use this to resync the Bitcoin blockchain from genesis',
warning: 'This will take a couple of days.',
'allowed-statuses': [
PackageMainStatus.Running,
PackageMainStatus.Stopped,
],
implementation: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
'input-spec': {
reason: {
type: 'string',
name: 'Re-sync Reason',
description: 'Your reason for re-syncing. Why are you doing this?',
placeholder: null,
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
textarea: false,
warning: null,
default: null,
},
name: {
type: 'string',
name: 'Your Name',
description: 'Tell the class your name.',
nullable: true,
masked: false,
warning: 'You may loose all your money by providing your name.',
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
default: null,
},
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
warning: null,
range: '[1,3]',
default: ['email'],
spec: {
'value-names': {
email: 'Email',
text: 'Text',
call: 'Call',
push: 'Push',
webhook: 'Webhook',
},
values: ['email', 'text', 'call', 'push', 'webhook'],
},
},
'days-ago': {
type: 'number',
name: 'Days Ago',
description: 'Number of days to re-sync.',
nullable: false,
default: 100,
range: '[0, 9999]',
integral: true,
units: null,
placeholder: null,
warning: null,
},
'top-speed': {
type: 'number',
name: 'Top Speed',
description: 'The fastest you can possibly run.',
nullable: false,
range: '[-1000, 1000]',
integral: false,
units: 'm/s',
placeholder: null,
warning: null,
default: null,
},
testnet: {
name: 'Testnet',
type: 'boolean',
description:
'<ul><li>determines whether your node is running on testnet or mainnet</li></ul><script src="fake"></script>',
warning: 'Chain will have to resync!',
default: false,
},
randomEnum: {
name: 'Random Enum',
type: 'enum',
'value-names': {
null: 'Null',
good: 'Good',
bad: 'Bad',
ugly: 'Ugly',
},
default: 'null',
description: 'This is not even real.',
warning: 'Be careful changing this!',
values: ['null', 'good', 'bad', 'ugly'],
},
'emergency-contact': {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
warning: null,
spec: {
name: {
type: 'string',
name: 'Name',
description: null,
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
email: {
type: 'string',
name: 'Email',
description: null,
nullable: false,
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
ips: {
name: 'Whitelist IPs',
type: 'list',
subtype: 'string',
description:
'external ip addresses that are authorized to access your Bitcoin node',
warning:
'Any IP you allow here will have RPC access to your Bitcoin node.',
range: '[1,10]',
default: ['192.168.1.1'],
spec: {
placeholder: null,
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
'pattern-description': 'Must be a valid IP address',
masked: false,
},
},
bitcoinNode: {
type: 'union',
default: 'internal',
tag: {
id: 'type',
'variant-names': {
internal: 'Internal',
external: 'External',
},
name: 'Bitcoin Node Settings',
description: 'The node settings',
warning: 'Careful changing this',
},
variants: {
internal: {
'friendly-name': {
name: 'Friendly Name',
type: 'string',
description: 'the lan address',
nullable: true,
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
external: {
'public-domain': {
name: 'Public Domain',
type: 'string',
description: 'the public address of the node',
nullable: false,
default: 'bitcoinnode.com',
pattern: '.*',
'pattern-description': 'anything',
masked: false,
placeholder: null,
textarea: false,
warning: null,
},
},
},
},
},
},
},
dependencies: {}, dependencies: {},
'os-version': '0.4.0',
} }
export const MockManifestLnd: Manifest = { export const MockManifestLnd: Manifest = {
@@ -377,15 +84,10 @@ export module Mock {
short: 'A bolt spec compliant client.', short: 'A bolt spec compliant client.',
long: 'More info about LND. More info about LND. More info about LND.', long: 'More info about LND. More info about LND. More info about LND.',
}, },
'release-notes': 'Dual funded channels!',
assets: { assets: {
icon: 'icon.png', icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
}, },
'release-notes': 'Dual funded channels!',
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd', 'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -400,104 +102,6 @@ export module Mock {
start: 'Starting LND is good for your health.', start: 'Starting LND is good for your health.',
stop: null, stop: null,
}, },
main: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': '10000µs',
},
config: {
get: null,
set: null,
},
volumes: {},
'min-os-version': '0.2.12',
interfaces: {
rpc: {
name: 'RPC interface',
description: 'Good for connecting to your node at a distance.',
ui: true,
'tor-config': {
'port-mapping': {},
},
'lan-config': {
'44': {
ssl: true,
mapping: 33,
},
},
protocols: [],
},
grpc: {
name: 'GRPC',
description: 'Certain wallet use grpc.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {
'66': {
ssl: true,
mapping: 55,
},
},
protocols: [],
},
},
backup: {
create: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
restore: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
},
migrations: null,
actions: {
resync: {
name: 'Resync Network Graph',
description: 'Your node will resync its network graph.',
warning: 'This will take a couple hours.',
'allowed-statuses': [PackageMainStatus.Running],
implementation: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
'input-spec': null,
},
},
dependencies: { dependencies: {
bitcoind: { bitcoind: {
version: '=0.21.0', version: '=0.21.0',
@@ -506,7 +110,6 @@ export module Mock {
type: 'opt-out', type: 'opt-out',
how: 'You can use an external node from your server if you prefer.', how: 'You can use an external node from your server if you prefer.',
}, },
config: null,
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
version: '>=0.2.2', version: '>=0.2.2',
@@ -516,9 +119,9 @@ export module Mock {
type: 'opt-in', type: 'opt-in',
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`, how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
}, },
config: null,
}, },
}, },
'os-version': '0.4.0',
} }
export const MockManifestBitcoinProxy: Manifest = { export const MockManifestBitcoinProxy: Manifest = {
@@ -530,15 +133,10 @@ export module Mock {
short: 'A super charger for your Bitcoin node.', short: 'A super charger for your Bitcoin node.',
long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.', long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.',
}, },
'release-notes': 'Even better support for Bitcoin and wallets!',
assets: { assets: {
icon: 'icon.png', icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
}, },
'release-notes': 'Even better support for Bitcoin and wallets!',
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper', 'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy', 'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -552,66 +150,6 @@ export module Mock {
start: null, start: null,
stop: null, stop: null,
}, },
main: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [''],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': '1m',
},
config: { get: {} as any, set: {} as any },
volumes: {},
'min-os-version': '0.2.12',
interfaces: {
rpc: {
name: 'RPC interface',
description: 'Good for connecting to your node at a distance.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {
44: {
ssl: true,
mapping: 33,
},
},
protocols: [],
},
},
backup: {
create: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [''],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
restore: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [''],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
},
migrations: null,
actions: {},
dependencies: { dependencies: {
bitcoind: { bitcoind: {
version: '>=0.20.0', version: '>=0.20.0',
@@ -619,34 +157,9 @@ export module Mock {
requirement: { requirement: {
type: 'required', type: 'required',
}, },
config: {
check: {
type: 'docker',
image: 'alpine',
system: true,
entrypoint: 'true',
args: [],
mounts: {},
'io-format': DockerIoFormat.Cbor,
inject: false,
'shm-size': '10m',
'sigterm-timeout': null,
},
'auto-configure': {
type: 'docker',
image: 'alpine',
system: true,
entrypoint: 'cat',
args: [],
mounts: {},
'io-format': DockerIoFormat.Cbor,
inject: false,
'shm-size': '10m',
'sigterm-timeout': null,
},
},
}, },
}, },
'os-version': '0.4.0',
} }
export const BitcoinDep: DependencyMetadata = { export const BitcoinDep: DependencyMetadata = {
@@ -1993,14 +1506,9 @@ export module Mock {
export const bitcoind: PackageDataEntry = { export const bitcoind: PackageDataEntry = {
state: PackageState.Installed, state: PackageState.Installed,
'static-files': {
license: '/public/package-data/bitcoind/0.20.0/LICENSE.md',
icon: '/assets/img/service-icons/bitcoind.svg',
instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md',
},
manifest: MockManifestBitcoind, manifest: MockManifestBitcoind,
icon: '/assets/img/service-icons/bitcoind.png',
installed: { installed: {
manifest: MockManifestBitcoind,
'last-backup': null, 'last-backup': null,
status: { status: {
configured: true, configured: true,
@@ -2011,35 +1519,62 @@ export module Mock {
}, },
'dependency-errors': {}, 'dependency-errors': {},
}, },
'interface-addresses': { 'address-info': {
ui: {
'tor-address': 'bitcoind-ui-address.onion',
'lan-address': 'bitcoind-ui-address.local',
},
rpc: { rpc: {
'tor-address': 'bitcoind-rpc-address.onion', name: 'Bitcoin RPC',
'lan-address': 'bitcoind-rpc-address.local', description: `Bitcoin's RPC interface`,
addresses: [
'http://bitcoind-rpc-address.onion',
'https://bitcoind-rpc-address.local',
'https://192.168.1.1:8332',
],
ui: true,
}, },
p2p: { p2p: {
'tor-address': 'bitcoind-p2p-address.onion', name: 'Bitcoin P2P',
'lan-address': 'bitcoind-p2p-address.local', description: `Bitcoin's P2P interface`,
addresses: [
'bitcoin://bitcoind-rpc-address.onion',
'bitcoin://192.168.1.1:8333',
],
ui: true,
}, },
}, },
'current-dependencies': {}, 'current-dependencies': {},
'dependency-info': {}, 'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key', 'developer-key': 'developer-key',
'has-config': true,
},
actions: {
resync: {
name: 'Resync Blockchain',
description: 'Use this to resync the Bitcoin blockchain from genesis',
warning: 'This will take a couple of days.',
disabled: null,
group: null,
'input-spec': {
reason: {
type: 'string',
name: 'Re-sync Reason',
description: 'Your reason for re-syncing. Why are you doing this?',
placeholder: null,
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
textarea: false,
warning: null,
default: null,
},
},
},
}, },
'install-progress': undefined,
} }
export const bitcoinProxy: PackageDataEntry = { export const bitcoinProxy: PackageDataEntry = {
state: PackageState.Installed, state: PackageState.Installed,
'static-files': { icon: '/assets/img/service-icons/btc-rpc-proxy.png',
license: '/public/package-data/btc-rpc-proxy/0.20.0/LICENSE.md',
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
instructions: '/public/package-data/btc-rpc-proxy/0.20.0/INSTRUCTIONS.md',
},
manifest: MockManifestBitcoinProxy, manifest: MockManifestBitcoinProxy,
installed: { installed: {
'last-backup': null, 'last-backup': null,
@@ -2050,11 +1585,15 @@ export module Mock {
}, },
'dependency-errors': {}, 'dependency-errors': {},
}, },
manifest: MockManifestBitcoinProxy, 'address-info': {
'interface-addresses': {
rpc: { rpc: {
'tor-address': 'bitcoinproxy-rpc-address.onion', name: 'Proxy RPC addresses',
'lan-address': 'bitcoinproxy-rpc-address.local', description: `Use these addresses to access Proxy's RPC interface`,
addresses: [
'http://bitcoinproxy-rpc-address.onion',
'https://bitcoinproxy-rpc-address.local',
],
ui: false,
}, },
}, },
'current-dependencies': { 'current-dependencies': {
@@ -2064,23 +1603,20 @@ export module Mock {
}, },
'dependency-info': { 'dependency-info': {
bitcoind: { bitcoind: {
manifest: Mock.MockManifestBitcoind, title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg', icon: 'assets/img/service-icons/bitcoind.svg',
}, },
}, },
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key', 'developer-key': 'developer-key',
'has-config': true,
}, },
'install-progress': undefined, actions: {},
} }
export const lnd: PackageDataEntry = { export const lnd: PackageDataEntry = {
state: PackageState.Installed, state: PackageState.Installed,
'static-files': { icon: '/assets/img/service-icons/lnd.png',
license: '/public/package-data/lnd/0.11.0/LICENSE.md',
icon: '/assets/img/service-icons/lnd.png',
instructions: '/public/package-data/lnd/0.11.0/INSTRUCTIONS.md',
},
manifest: MockManifestLnd, manifest: MockManifestLnd,
installed: { installed: {
'last-backup': null, 'last-backup': null,
@@ -2095,15 +1631,26 @@ export module Mock {
}, },
}, },
}, },
manifest: MockManifestLnd, 'address-info': {
'interface-addresses': { ui: {
rpc: { name: 'Web UI',
'tor-address': 'lnd-rpc-address.onion', description: 'The browser web interface for LND',
'lan-address': 'lnd-rpc-address.local', addresses: [
'http://lnd-ui-address.onion',
'https://lnd-ui-address.local',
'https://192.168.1.1:3449',
],
ui: true,
}, },
grpc: { grpc: {
'tor-address': 'lnd-grpc-address.onion', name: 'gRPC',
'lan-address': 'lnd-grpc-address.local', description: 'For connecting to LND gRPC interface',
addresses: [
'http://lnd-grpc-address.onion',
'https://lnd-grpc-address.local',
'https://192.168.1.1:3449',
],
ui: true,
}, },
}, },
'current-dependencies': { 'current-dependencies': {
@@ -2116,23 +1663,24 @@ export module Mock {
}, },
'dependency-info': { 'dependency-info': {
bitcoind: { bitcoind: {
manifest: Mock.MockManifestBitcoind, title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg', icon: 'assets/img/service-icons/bitcoind.svg',
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
manifest: Mock.MockManifestBitcoinProxy, title: 'Bitcoin Proxy',
icon: 'assets/img/service-icons/btc-rpc-proxy.png', icon: 'assets/img/service-icons/btc-rpc-proxy.png',
}, },
}, },
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key', 'developer-key': 'developer-key',
'has-config': true,
}, },
'install-progress': undefined, actions: {},
} }
export const LocalPkgs: { [key: string]: PackageDataEntry } = { export const LocalPkgs: { [key: string]: PackageDataEntry } = {
bitcoind: bitcoind, bitcoind,
'btc-rpc-proxy': bitcoinProxy, 'btc-rpc-proxy': bitcoinProxy,
lnd: lnd, lnd,
} }
} }

View File

@@ -1,11 +1,10 @@
import { Dump, Revision } from 'patch-db-client' import { Dump, Revision } from 'patch-db-client'
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
import { InputSpec } from 'start-sdk/types/config-types' import { InputSpec } from 'start-sdk/types/config-types'
import { import {
DataModel, DataModel,
DependencyError, DependencyError,
Manifest,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'

View File

@@ -15,7 +15,6 @@ import {
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
ServerStatus,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { CifsBackupTarget, RR } from './api.types' import { CifsBackupTarget, RR } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { parsePropertiesPermissive } from 'src/app/util/properties.util'
@@ -999,11 +998,11 @@ export class MockApiService extends ApiService {
this.mockRevision(patch2) this.mockRevision(patch2)
setTimeout(async () => { setTimeout(async () => {
const patch3: Operation<ServerStatus>[] = [ const patch3: Operation<boolean>[] = [
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: '/server-info/status', path: '/server-info/status-info/updated',
value: ServerStatus.Updated, value: true,
}, },
{ {
op: PatchOp.REMOVE, op: PatchOp.REMOVE,
@@ -1011,16 +1010,6 @@ export class MockApiService extends ApiService {
}, },
] ]
this.mockRevision(patch3) this.mockRevision(patch3)
// quickly revert server to "running" for continued testing
await pauseFor(100)
const patch4 = [
{
op: PatchOp.REPLACE,
path: '/server-info/status',
value: ServerStatus.Running,
},
]
this.mockRevision(patch4)
// set patch indicating update is complete // set patch indicating update is complete
await pauseFor(100) await pauseFor(100)
const patch6 = [ const patch6 = [

View File

@@ -1,13 +1,10 @@
import { import {
DataModel, DataModel,
DependencyErrorType, DependencyErrorType,
DockerIoFormat,
HealthResult, HealthResult,
Manifest,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { Mock } from './api.fixures'
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets' import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
export const mockPatchData: DataModel = { export const mockPatchData: DataModel = {
@@ -68,6 +65,7 @@ export const mockPatchData: DataModel = {
'backup-progress': null, 'backup-progress': null,
updated: false, updated: false,
'update-progress': null, 'update-progress': null,
'shutting-down': false,
}, },
hostname: 'random-words', hostname: 'random-words',
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
@@ -78,11 +76,7 @@ export const mockPatchData: DataModel = {
'package-data': { 'package-data': {
bitcoind: { bitcoind: {
state: PackageState.Installed, state: PackageState.Installed,
'static-files': { icon: '/assets/img/service-icons/bitcoind.svg',
license: '/public/package-data/bitcoind/0.20.0/LICENSE.md',
icon: '/assets/img/service-icons/bitcoind.svg',
instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md',
},
manifest: { manifest: {
id: 'bitcoind', id: 'bitcoind',
title: 'Bitcoin Core', title: 'Bitcoin Core',
@@ -92,15 +86,10 @@ export const mockPatchData: DataModel = {
short: 'A Bitcoin full node by Bitcoin Core.', short: 'A Bitcoin full node by Bitcoin Core.',
long: 'Bitcoin is a decentralized consensus protocol and settlement network.', long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
}, },
'release-notes': 'Taproot, Schnorr, and more.',
assets: { assets: {
icon: 'icon.png', icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
}, },
'release-notes': 'Taproot, Schnorr, and more.',
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'upstream-repo': 'https://github.com/bitcoin/bitcoin', 'upstream-repo': 'https://github.com/bitcoin/bitcoin',
@@ -115,308 +104,10 @@ export const mockPatchData: DataModel = {
start: 'Starting Bitcoin is good for your health.', start: 'Starting Bitcoin is good for your health.',
stop: null, stop: null,
}, },
main: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': '.49m',
},
config: {
get: {},
set: {},
} as any,
volumes: {},
'min-os-version': '0.2.12',
interfaces: {
ui: {
name: 'Node Visualizer',
description:
'Web application for viewing information about your node and the Bitcoin network.',
ui: true,
'tor-config': {
'port-mapping': {},
},
'lan-config': {},
protocols: [],
},
rpc: {
name: 'RPC',
description:
'Used by wallets to interact with your Bitcoin Core node.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {},
protocols: [],
},
p2p: {
name: 'P2P',
description:
'Used by other Bitcoin nodes to communicate and interact with your node.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {},
protocols: [],
},
},
backup: {
create: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
restore: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
},
migrations: null,
actions: {
resync: {
name: 'Resync Blockchain',
description:
'Use this to resync the Bitcoin blockchain from genesis',
warning: 'This will take a couple of days.',
'allowed-statuses': [
PackageMainStatus.Running,
PackageMainStatus.Stopped,
],
implementation: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
'input-spec': {
reason: {
type: 'string',
name: 'Re-sync Reason',
description:
'Your reason for re-syncing. Why are you doing this?',
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
name: {
type: 'string',
name: 'Your Name',
description: 'Tell the class your name.',
nullable: true,
masked: false,
warning: 'You may loose all your money by providing your name.',
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
default: null,
},
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
warning: null,
range: '[1,3]',
default: ['email'],
spec: {
'value-names': {
email: 'Email',
text: 'Text',
call: 'Call',
push: 'Push',
webhook: 'Webhook',
},
values: ['email', 'text', 'call', 'push', 'webhook'],
},
},
'days-ago': {
type: 'number',
name: 'Days Ago',
description: 'Number of days to re-sync.',
nullable: false,
default: 100,
range: '[0, 9999]',
integral: true,
units: null,
placeholder: null,
warning: null,
},
'top-speed': {
type: 'number',
name: 'Top Speed',
description: 'The fastest you can possibly run.',
nullable: false,
range: '[-1000, 1000]',
integral: false,
units: 'm/s',
placeholder: null,
warning: null,
default: null,
},
testnet: {
name: 'Testnet',
type: 'boolean',
description:
'<ul><li>determines whether your node is running on testnet or mainnet</li></ul><script src="fake"></script>',
warning: 'Chain will have to resync!',
default: false,
},
randomEnum: {
name: 'Random Enum',
type: 'enum',
'value-names': {
null: 'Null',
good: 'Good',
bad: 'Bad',
ugly: 'Ugly',
},
default: 'null',
description: 'This is not even real.',
warning: 'Be careful changing this!',
values: ['null', 'good', 'bad', 'ugly'],
},
'emergency-contact': {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
warning: null,
spec: {
name: {
type: 'string',
name: 'Name',
description: null,
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
email: {
type: 'string',
name: 'Email',
description: null,
nullable: false,
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
},
ips: {
name: 'Whitelist IPs',
type: 'list',
subtype: 'string',
description:
'external ip addresses that are authorized to access your Bitcoin node',
warning:
'Any IP you allow here will have RPC access to your Bitcoin node.',
range: '[1,10]',
default: ['192.168.1.1'],
spec: {
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
'pattern-description': 'Must be a valid IP address',
masked: false,
placeholder: null,
},
},
bitcoinNode: {
type: 'union',
default: 'internal',
tag: {
id: 'type',
'variant-names': {
internal: 'Internal',
external: 'External',
},
name: 'Bitcoin Node Settings',
description: 'The node settings',
warning: 'Careful changing this',
},
variants: {
internal: {
'friendly-name': {
name: 'Friendly Name',
type: 'string',
description: 'the lan address',
nullable: true,
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
},
external: {
'public-domain': {
name: 'Public Domain',
type: 'string',
description: 'the public address of the node',
nullable: false,
default: 'bitcoinnode.com',
pattern: '.*',
'pattern-description': 'anything',
masked: false,
placeholder: null,
textarea: false,
warning: null,
},
},
},
},
},
},
},
dependencies: {}, dependencies: {},
'os-version': '0.4.0',
}, },
installed: { installed: {
manifest: {
...Mock.MockManifestBitcoind,
version: '0.20.0',
},
'last-backup': null, 'last-backup': null,
status: { status: {
configured: true, configured: true,
@@ -446,38 +137,43 @@ export const mockPatchData: DataModel = {
'unnecessary-health-check': { 'unnecessary-health-check': {
name: 'Totally Unnecessary', name: 'Totally Unnecessary',
result: HealthResult.Disabled, result: HealthResult.Disabled,
reason: 'You disabled this on purpose',
}, },
}, },
}, },
'dependency-errors': {}, 'dependency-errors': {},
}, },
'interface-addresses': { 'address-info': {
ui: {
'tor-address': 'bitcoind-ui-address.onion',
'lan-address': 'bitcoind-ui-address.local',
},
rpc: { rpc: {
'tor-address': 'bitcoind-rpc-address.onion', name: 'Bitcoin RPC',
'lan-address': 'bitcoind-rpc-address.local', description: `Bitcoin's RPC interface`,
addresses: [
'http://bitcoind-rpc-address.onion',
'https://bitcoind-rpc-address.local',
'https://192.168.1.1:8332',
],
ui: true,
}, },
p2p: { p2p: {
'tor-address': 'bitcoind-p2p-address.onion', name: 'Bitcoin P2P',
'lan-address': 'bitcoind-p2p-address.local', description: `Bitcoin's P2P interface`,
addresses: [
'bitcoin://bitcoind-rpc-address.onion',
'bitcoin://192.168.1.1:8333',
],
ui: true,
}, },
}, },
'current-dependencies': {}, 'current-dependencies': {},
'dependency-info': {}, 'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key', 'developer-key': 'developer-key',
'has-config': true,
}, },
}, },
lnd: { lnd: {
state: PackageState.Installed, state: PackageState.Installed,
'static-files': { icon: '/assets/img/service-icons/lnd.png',
license: '/public/package-data/lnd/0.11.1/LICENSE.md',
icon: '/assets/img/service-icons/lnd.png',
instructions: '/public/package-data/lnd/0.11.1/INSTRUCTIONS.md',
},
manifest: { manifest: {
id: 'lnd', id: 'lnd',
title: 'Lightning Network Daemon', title: 'Lightning Network Daemon',
@@ -486,15 +182,10 @@ export const mockPatchData: DataModel = {
short: 'A bolt spec compliant client.', short: 'A bolt spec compliant client.',
long: 'More info about LND. More info about LND. More info about LND.', long: 'More info about LND. More info about LND. More info about LND.',
}, },
'release-notes': 'Dual funded channels!',
assets: { assets: {
icon: 'icon.png', icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
}, },
'release-notes': 'Dual funded channels!',
license: 'MIT', license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd', 'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -509,104 +200,6 @@ export const mockPatchData: DataModel = {
start: 'Starting LND is good for your health.', start: 'Starting LND is good for your health.',
stop: null, stop: null,
}, },
main: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': '0.5s',
},
config: {
get: null,
set: null,
},
volumes: {},
'min-os-version': '0.2.12',
interfaces: {
rpc: {
name: 'RPC interface',
description: 'Good for connecting to your node at a distance.',
ui: true,
'tor-config': {
'port-mapping': {},
},
'lan-config': {
'44': {
ssl: true,
mapping: 33,
},
},
protocols: [],
},
grpc: {
name: 'GRPC',
description: 'Certain wallet use grpc.',
ui: false,
'tor-config': {
'port-mapping': {},
},
'lan-config': {
'66': {
ssl: true,
mapping: 55,
},
},
protocols: [],
},
},
backup: {
create: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
restore: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
},
migrations: null,
actions: {
resync: {
name: 'Resync Network Graph',
description: 'Your node will resync its network graph.',
warning: 'This will take a couple hours.',
'allowed-statuses': [PackageMainStatus.Running],
implementation: {
type: 'docker',
image: '',
system: true,
entrypoint: '',
args: [],
mounts: {},
'io-format': DockerIoFormat.Yaml,
inject: false,
'shm-size': '',
'sigterm-timeout': null,
},
'input-spec': null,
},
},
dependencies: { dependencies: {
bitcoind: { bitcoind: {
version: '=0.21.0', version: '=0.21.0',
@@ -615,7 +208,6 @@ export const mockPatchData: DataModel = {
type: 'opt-out', type: 'opt-out',
how: 'You can use an external node from your server if you prefer.', how: 'You can use an external node from your server if you prefer.',
}, },
config: null,
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
version: '>=0.2.2', version: '>=0.2.2',
@@ -625,9 +217,9 @@ export const mockPatchData: DataModel = {
type: 'opt-in', type: 'opt-in',
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`, how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
}, },
config: null,
}, },
}, },
'os-version': '0.4.0',
}, },
installed: { installed: {
manifest: { manifest: {
@@ -647,14 +239,26 @@ export const mockPatchData: DataModel = {
}, },
}, },
}, },
'interface-addresses': { 'address-info': {
rpc: { ui: {
'tor-address': 'lnd-rpc-address.onion', name: 'Web UI',
'lan-address': 'lnd-rpc-address.local', description: 'The browser web interface for LND',
addresses: [
'http://lnd-ui-address.onion',
'https://lnd-ui-address.local',
'https://192.168.1.1:3449',
],
ui: true,
}, },
grpc: { grpc: {
'tor-address': 'lnd-grpc-address.onion', name: 'gRPC',
'lan-address': 'lnd-grpc-address.local', description: 'For connecting to LND gRPC interface',
addresses: [
'http://lnd-grpc-address.onion',
'https://lnd-grpc-address.local',
'https://192.168.1.1:3449',
],
ui: true,
}, },
}, },
'current-dependencies': { 'current-dependencies': {
@@ -667,20 +271,17 @@ export const mockPatchData: DataModel = {
}, },
'dependency-info': { 'dependency-info': {
bitcoind: { bitcoind: {
manifest: { title: 'Bitcoin Core',
title: 'Bitcoin Core',
} as Manifest,
icon: 'assets/img/service-icons/bitcoind.svg', icon: 'assets/img/service-icons/bitcoind.svg',
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
manifest: { title: 'Bitcoin Proxy',
title: 'Bitcoin Proxy',
} as Manifest,
icon: 'assets/img/service-icons/btc-rpc-proxy.png', icon: 'assets/img/service-icons/btc-rpc-proxy.png',
}, },
}, },
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key', 'developer-key': 'developer-key',
'has-config': true,
}, },
}, },
}, },

View File

@@ -2,10 +2,8 @@ import { DOCUMENT } from '@angular/common'
import { Inject, Injectable } from '@angular/core' import { Inject, Injectable } from '@angular/core'
import { WorkspaceConfig } from '@start9labs/shared' import { WorkspaceConfig } from '@start9labs/shared'
import { import {
InterfaceDef, InstalledPackageInfo,
PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
const { const {
@@ -52,56 +50,12 @@ export class ConfigService {
isSecure(): boolean { isSecure(): boolean {
return window.isSecureContext || this.isTor() return window.isSecureContext || this.isTor()
} }
isLaunchable(
state: PackageState,
status: PackageMainStatus,
interfaces: Record<string, InterfaceDef>,
): boolean {
return (
state === PackageState.Installed &&
status === PackageMainStatus.Running &&
hasUi(interfaces)
)
}
launchableURL(pkg: PackageDataEntry): string {
if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) {
return `https://${lanUiAddress(pkg)}`
} else {
return `http://${torUiAddress(pkg)}`
}
}
} }
export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean { export function hasUi(
const int = getUiInterfaceValue(interfaces) addressInfo: InstalledPackageInfo['address-info'],
return !!int?.['tor-config'] ): boolean {
} return !!Object.values(addressInfo).find(a => a.ui)
export function hasLanUi(interfaces: Record<string, InterfaceDef>): boolean {
const int = getUiInterfaceValue(interfaces)
return !!int?.['lan-config']
}
export function torUiAddress({
manifest,
installed,
}: PackageDataEntry): string {
const key = getUiInterfaceKey(manifest.interfaces)
return installed ? installed['interface-addresses'][key]['tor-address'] : ''
}
export function lanUiAddress({
manifest,
installed,
}: PackageDataEntry): string {
const key = getUiInterfaceKey(manifest.interfaces)
return installed ? installed['interface-addresses'][key]['lan-address'] : ''
}
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
return hasTorUi(interfaces) || hasLanUi(interfaces)
} }
export function removeProtocol(str: string): string { export function removeProtocol(str: string): string {
@@ -113,15 +67,3 @@ export function removeProtocol(str: string): string {
export function removePort(str: string): string { export function removePort(str: string): string {
return str.split(':')[0] return str.split(':')[0]
} }
export function getUiInterfaceKey(
interfaces: Record<string, InterfaceDef>,
): string {
return Object.keys(interfaces).find(key => interfaces[key].ui) || ''
}
export function getUiInterfaceValue(
interfaces: Record<string, InterfaceDef>,
): InterfaceDef | null {
return Object.values(interfaces).find(i => i.ui) || null
}

View File

@@ -1,6 +1,6 @@
import { InputSpec } from 'start-sdk/types/config-types' import { InputSpec } from 'start-sdk/types/config-types'
import { Url } from '@start9labs/shared' import { Url } from '@start9labs/shared'
import { MarketplaceManifest } from '@start9labs/marketplace' import { Manifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
export interface DataModel { export interface DataModel {
@@ -11,7 +11,7 @@ export interface DataModel {
export interface UIData { export interface UIData {
name: string | null name: string | null
'ack-welcome': string // eOS emver 'ack-welcome': string // emver
marketplace: UIMarketplaceData marketplace: UIMarketplaceData
dev: DevData dev: DevData
gaming: { gaming: {
@@ -95,176 +95,98 @@ export interface ServerStatusInfo {
} }
updated: boolean updated: boolean
'update-progress': { size: number | null; downloaded: number } | null 'update-progress': { size: number | null; downloaded: number } | null
} 'shutting-down': boolean
export enum ServerStatus {
Running = 'running',
Updated = 'updated',
BackingUp = 'backing-up',
} }
export interface PackageDataEntry { export interface PackageDataEntry {
state: PackageState state: PackageState
'static-files': {
license: Url
instructions: Url
icon: Url
}
manifest: Manifest manifest: Manifest
installed?: InstalledPackageDataEntry // exists when: installed, updating icon: string
'install-progress'?: InstallProgress // exists when: installing, updating installed?: InstalledPackageInfo // when: installed
actions?: Record<string, Action> // when: installed
'install-progress'?: InstallProgress // when: installing, updating, restoring
} }
// export type PackageDataEntry =
// | PackageDataEntryInstalled
// | PackageDataEntryNeedsUpdate
// | PackageDataEntryRemoving
// | PackageDataEntryRestoring
// | PackageDataEntryUpdating
// | PackageDataEntryInstalling
// export type PackageDataEntryBase = {
// manifest: Manifest
// icon: Url
// }
// export interface PackageDataEntryInstalled extends PackageDataEntryBase {
// state: PackageState.Installed
// installed: InstalledPackageInfo
// actions: Record<string, Action>
// }
// export interface PackageDataEntryNeedsUpdate extends PackageDataEntryBase {
// state: PackageState.NeedsUpdate
// }
// export interface PackageDataEntryRemoving extends PackageDataEntryBase {
// state: PackageState.Removing
// }
// export interface PackageDataEntryRestoring extends PackageDataEntryBase {
// state: PackageState.Restoring
// 'install-progress': InstallProgress
// }
// export interface PackageDataEntryUpdating extends PackageDataEntryBase {
// state: PackageState.Updating
// 'install-progress': InstallProgress
// }
// export interface PackageDataEntryInstalling extends PackageDataEntryBase {
// state: PackageState.Installing
// 'install-progress': InstallProgress
// }
export enum PackageState { export enum PackageState {
Installing = 'installing', Installing = 'installing',
Installed = 'installed', Installed = 'installed',
Updating = 'updating', Updating = 'updating',
Removing = 'removing', Removing = 'removing',
Restoring = 'restoring', Restoring = 'restoring',
NeedsUpdate = 'needs-update',
} }
export interface InstalledPackageDataEntry { export interface InstalledPackageInfo {
status: Status status: Status
manifest: Manifest
'last-backup': string | null 'last-backup': string | null
'current-dependencies': { [id: string]: CurrentDependencyInfo } 'current-dependencies': Record<string, CurrentDependencyInfo>
'dependency-info': { 'dependency-info': Record<string, { title: string; icon: Url }>
[id: string]: { 'address-info': Record<string, AddressInfo>
manifest: Manifest
icon: Url
}
}
'interface-addresses': {
[id: string]: { 'tor-address': string; 'lan-address': string }
}
'marketplace-url': string | null 'marketplace-url': string | null
'developer-key': string 'developer-key': string
'has-config': boolean
} }
export interface CurrentDependencyInfo { export interface CurrentDependencyInfo {
'health-checks': string[] // array of health check IDs 'health-checks': string[] // array of health check IDs
} }
export interface Manifest extends MarketplaceManifest<DependencyConfig | null> { export interface AddressInfo {
assets: {
license: string // filename
instructions: string // filename
icon: string // filename
docker_images: string // filename
assets: string // path to assets folder
scripts: string // path to scripts folder
}
main: ActionImpl
config: ConfigActions | null
volumes: Record<string, Volume>
'min-os-version': string
interfaces: Record<string, InterfaceDef>
backup: BackupActions
migrations: Migrations | null
actions: Record<string, Action>
}
export interface DependencyConfig {
check: ActionImpl
'auto-configure': ActionImpl
}
export interface ActionImpl {
type: 'docker'
image: string
system: boolean
entrypoint: string
args: string[]
mounts: { [id: string]: string }
'io-format': DockerIoFormat | null
inject: boolean
'shm-size': string
'sigterm-timeout': string | null
}
export enum DockerIoFormat {
Json = 'json',
Yaml = 'yaml',
Cbor = 'cbor',
Toml = 'toml',
}
export interface ConfigActions {
get: ActionImpl | null
set: ActionImpl | null
}
export type Volume = VolumeData
export interface VolumeData {
type: VolumeType.Data
readonly: boolean
}
export interface VolumeAssets {
type: VolumeType.Assets
}
export interface VolumePointer {
type: VolumeType.Pointer
'package-id': string
'volume-id': string
path: string
readonly: boolean
}
export interface VolumeCertificate {
type: VolumeType.Certificate
'interface-id': string
}
export interface VolumeBackup {
type: VolumeType.Backup
readonly: boolean
}
export enum VolumeType {
Data = 'data',
Assets = 'assets',
Pointer = 'pointer',
Certificate = 'certificate',
Backup = 'backup',
}
export interface InterfaceDef {
name: string name: string
description: string description: string
'tor-config': TorConfig | null addresses: Url[]
'lan-config': LanConfig | null
ui: boolean ui: boolean
protocols: string[]
}
export interface TorConfig {
'port-mapping': { [port: number]: number }
}
export type LanConfig = {
[port: number]: { ssl: boolean; mapping: number }
}
export interface BackupActions {
create: ActionImpl
restore: ActionImpl
}
export interface Migrations {
from: { [versionRange: string]: ActionImpl }
to: { [versionRange: string]: ActionImpl }
} }
export interface Action { export interface Action {
name: string name: string
description: string description: string
warning: string | null warning: string | null
implementation: ActionImpl disabled: string | null
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
'input-spec': InputSpec | null 'input-spec': InputSpec | null
group: string | null
} }
export interface Status { export interface Status {
@@ -280,6 +202,7 @@ export type MainStatus =
| MainStatusRunning | MainStatusRunning
| MainStatusBackingUp | MainStatusBackingUp
| MainStatusRestarting | MainStatusRestarting
| MainStatusConfiguring
export interface MainStatusStopped { export interface MainStatusStopped {
status: PackageMainStatus.Stopped status: PackageMainStatus.Stopped
@@ -301,13 +224,16 @@ export interface MainStatusRunning {
export interface MainStatusBackingUp { export interface MainStatusBackingUp {
status: PackageMainStatus.BackingUp status: PackageMainStatus.BackingUp
started: string | null // UTC date string
} }
export interface MainStatusRestarting { export interface MainStatusRestarting {
status: PackageMainStatus.Restarting status: PackageMainStatus.Restarting
} }
export interface MainStatusConfiguring {
status: PackageMainStatus.Configuring
}
export enum PackageMainStatus { export enum PackageMainStatus {
Starting = 'starting', Starting = 'starting',
Running = 'running', Running = 'running',
@@ -315,6 +241,7 @@ export enum PackageMainStatus {
Stopped = 'stopped', Stopped = 'stopped',
BackingUp = 'backing-up', BackingUp = 'backing-up',
Restarting = 'restarting', Restarting = 'restarting',
Configuring = 'configuring',
} }
export type HealthCheckResult = { name: string } & ( export type HealthCheckResult = { name: string } & (
@@ -339,6 +266,7 @@ export interface HealthCheckResultStarting {
export interface HealthCheckResultDisabled { export interface HealthCheckResultDisabled {
result: HealthResult.Disabled result: HealthResult.Disabled
reason: string
} }
export interface HealthCheckResultSuccess { export interface HealthCheckResultSuccess {
@@ -370,7 +298,6 @@ export enum DependencyErrorType {
IncorrectVersion = 'incorrect-version', IncorrectVersion = 'incorrect-version',
ConfigUnsatisfied = 'config-unsatisfied', ConfigUnsatisfied = 'config-unsatisfied',
HealthChecksFailed = 'health-checks-failed', HealthChecksFailed = 'health-checks-failed',
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
Transitive = 'transitive', Transitive = 'transitive',
} }

View File

@@ -1,6 +1,6 @@
import { isEmptyObject } from '@start9labs/shared' import { isEmptyObject } from '@start9labs/shared'
import { import {
InstalledPackageDataEntry, InstalledPackageInfo,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
@@ -38,7 +38,7 @@ function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus {
} }
function getDependencyStatus( function getDependencyStatus(
installed: InstalledPackageDataEntry, installed: InstalledPackageInfo,
): DependencyStatus | null { ): DependencyStatus | null {
if (isEmptyObject(installed['current-dependencies'])) return null if (isEmptyObject(installed['current-dependencies'])) return null

View File

@@ -1,22 +1,27 @@
import { Inject, Injectable } from '@angular/core' import { Inject, Injectable } from '@angular/core'
import { DOCUMENT } from '@angular/common' import { DOCUMENT } from '@angular/common'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
import { ConfigService } from './config.service'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class UiLauncherService { export class UiLauncherService {
constructor( constructor(@Inject(DOCUMENT) private readonly document: Document) {}
@Inject(DOCUMENT) private readonly document: Document,
private readonly config: ConfigService,
) {}
launch(pkg: PackageDataEntry): void { launch(addressInfo: InstalledPackageInfo['address-info']): void {
this.document.defaultView?.open( const UIs = Object.values(addressInfo)
this.config.launchableURL(pkg), .filter(info => info.ui)
'_blank', .map(info => ({
'noreferrer', name: info.name,
) addresses: info.addresses,
}))
if (UIs.length === 1 && UIs[0].addresses.length === 1) {
this.document.defaultView?.open(
UIs[0].addresses[0],
'_blank',
'noreferrer',
)
}
} }
} }