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">
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<p class="published">
Released: {{ pkg['published-at'] | date: 'medium' }}
<p *ngIf="pkg['published-at'] as published" class="published">
Released: {{ published | date : 'medium' }}
</p>
<ng-content></ng-content>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,13 +9,6 @@
<span *ngIf="!installProgress">
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
<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 *ngIf="installProgress">
<ion-text
@@ -23,7 +16,8 @@
color="primary"
>
Installing
<span class="loading-dots"></span>{{ progress }}
<span class="loading-dots"></span>
{{ progress }}
</ion-text>
</span>
</p>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'
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 { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs'
@@ -36,7 +36,7 @@ export class BackupSelectPage {
return {
id,
title,
icon: pkg['static-files'].icon,
icon: pkg.icon,
disabled: 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 { Routes, RouterModule } from '@angular/router'
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 { SharedPipesModule } from '@start9labs/shared'
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
@@ -25,6 +29,6 @@ const routes: Routes = [
GenericFormPageModule,
ActionSuccessPageModule,
],
declarations: [AppActionsPage, AppActionsItemComponent],
declarations: [AppActionsPage, AppActionsItemComponent, GroupActionsPipe],
})
export class AppActionsPageModule {}

View File

@@ -21,17 +21,19 @@
></app-actions-item>
<!-- ** specific actions ** -->
<ion-item-divider *ngIf="!(pkg.manifest.actions | empty)">
Actions for {{ pkg.manifest.title }}
</ion-item-divider>
<app-actions-item
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder"
[action]="{
name: action.value.name,
description: action.value.description,
icon: 'play-circle-outline'
}"
(click)="handleAction(pkg, action)"
></app-actions-item>
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
<ion-item-divider>Actions for {{ pkg.manifest.title }}</ion-item-divider>
<div *ngFor="let group of actionGroups" class="ion-padding-bottm">
<app-actions-item
*ngFor="let action of group"
[action]="{
name: action.name,
description: action.description,
icon: 'play-circle-outline'
}"
(click)="handleAction(action)"
></app-actions-item>
</div>
</ng-container>
</ion-item-group>
</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 { ApiService } from 'src/app/services/api/embassy-api.service'
import {
@@ -12,12 +18,18 @@ import {
Action,
DataModel,
PackageDataEntry,
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
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 { hasCurrentDeps } from 'src/app/util/has-deps'
import { filter } from 'rxjs'
@Component({
selector: 'app-actions',
@@ -27,7 +39,9 @@ import { hasCurrentDeps } from 'src/app/util/has-deps'
})
export class AppActionsPage {
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(
private readonly route: ActivatedRoute,
@@ -40,28 +54,27 @@ export class AppActionsPage {
private readonly patch: PatchDB<DataModel>,
) {}
async handleAction(
pkg: PackageDataEntry,
action: { key: string; value: Action },
) {
const status = pkg.installed?.status
if (
status &&
(action.value['allowed-statuses'] as PackageMainStatus[]).includes(
status.main.status,
)
) {
if (!isEmptyObject(action.value['input-spec'] || {})) {
async handleAction(action: WithId<Action>) {
if (action.disabled) {
const alert = await this.alertCtrl.create({
header: 'Forbidden',
message: action.disabled,
buttons: ['OK'],
cssClass: 'alert-error-message enter-click',
})
await alert.present()
} else {
if (!isEmptyObject(action['input-spec'] || {})) {
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: action.value.name,
spec: action.value['input-spec'],
title: action.name,
spec: action['input-spec'],
buttons: [
{
text: 'Execute',
handler: (value: any) => {
return this.executeAction(action.key, value)
return this.executeAction(action.id, value)
},
isSubmit: true,
},
@@ -72,9 +85,9 @@ export class AppActionsPage {
} else {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: `Are you sure you want to execute action "${
action.value.name
}"? ${action.value.warning || ''}`,
message: `Are you sure you want to execute action "${action.name}"? ${
action.warning || ''
}`,
buttons: [
{
text: 'Cancel',
@@ -83,7 +96,7 @@ export class AppActionsPage {
{
text: 'Execute',
handler: () => {
this.executeAction(action.key)
this.executeAction(action.id)
},
cssClass: 'enter-click',
},
@@ -91,31 +104,6 @@ export class AppActionsPage {
})
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 {
@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
slot="start"
size="large"
[name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'"
[name]="addressInfo.ui ? 'desktop-outline' : 'terminal-outline'"
></ion-icon>
<ion-label>
<h1>{{ interface.def.name }}</h1>
<h2>{{ interface.def.description }}</h2>
<h1>{{ addressInfo.name }}</h1>
<h2>{{ addressInfo.description }}</h2>
</ion-label>
</ion-item>
<div *ngIf="interface" style="padding-left: 64px">
<!-- has tor -->
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
<div style="padding-left: 64px">
<ion-item *ngFor="let address of addressInfo.addresses">
<ion-label>
<h2>Tor Address</h2>
<p>{{ tor }}</p>
<h2>{{ address | addressType }}</h2>
<p>{{ address }}</p>
</ion-label>
<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-button>
<ion-button fill="clear" (click)="showQR(tor)">
<ion-button fill="clear" (click)="showQR(address)">
<ion-icon
size="small"
slot="icon-only"
name="qr-code-outline"
></ion-icon>
</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-button>
</ion-buttons>
</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>

View File

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

View File

@@ -9,18 +9,11 @@
<ion-content class="ion-padding-top with-widgets">
<ion-item-group>
<!-- iff ui -->
<ng-container *ngIf="ui">
<ion-item-divider>User Interface</ion-item-divider>
<app-interfaces-item [interface]="ui"></app-interfaces-item>
</ng-container>
<!-- 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>
<div
*ngFor="let addressInfo of (addressInfo$ | async)"
style="margin-bottom: 30px"
>
<app-interfaces-item [addressInfo]="addressInfo"></app-interfaces-item>
</div>
</ion-item-group>
</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 { ModalController, ToastController } from '@ionic/angular'
import { getPkgId, copyToClipboard } from '@start9labs/shared'
import { getUiInterfaceKey } from 'src/app/services/config.service'
import {
DataModel,
InstalledPackageDataEntry,
InterfaceDef,
} from 'src/app/services/patch-db/data-model'
import { AddressInfo, DataModel } from 'src/app/services/patch-db/data-model'
import { PatchDB } from 'patch-db-client'
import { QRComponent } from 'src/app/components/qr/qr.component'
import { getPackage } from '../../../util/get-package-data'
interface LocalInterface {
def: InterfaceDef
addresses: InstalledPackageDataEntry['interface-addresses'][string]
}
import { map } from 'rxjs'
@Component({
selector: 'app-interfaces',
templateUrl: './app-interfaces.page.html',
styleUrls: ['./app-interfaces.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppInterfacesPage {
ui?: LocalInterface
other: LocalInterface[] = []
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(
private readonly route: ActivatedRoute,
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({
@@ -84,7 +42,7 @@ export class AppInterfacesPage {
})
export class AppInterfacesItemComponent {
@Input()
interface!: LocalInterface
addressInfo!: AddressInfo
constructor(
private readonly toastCtrl: ToastController,
@@ -122,3 +80,31 @@ export class AppInterfacesItemComponent {
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>
<ion-thumbnail slot="start">
<img alt="" [src]="pkg.entry['static-files'].icon" />
<img alt="" [src]="pkg.entry.icon" />
</ion-thumbnail>
<ion-label>
<h2 ticker>{{ manifest.title }}</h2>
@@ -17,17 +17,18 @@
[installProgress]="pkg.entry['install-progress']"
weight="bold"
size="small"
[sigtermTimeout]="manifest.main['sigterm-timeout']"
></status>
</ion-label>
<ion-button
*ngIf="manifest.interfaces | hasUi"
slot="end"
fill="clear"
color="primary"
(click)="launchUi($event)"
[disabled]="!(pkg.entry.state | isLaunchable: status:manifest.interfaces)"
>
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
<ng-container *ngIf="pkg.entry.installed as installed">
<ion-button
*ngIf="installed['address-info'] | hasUi"
slot="end"
fill="clear"
color="primary"
(click)="launchUi($event, installed['address-info'])"
[disabled]="status !== 'running'"
>
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
</ion-button>
</ng-container>
</ion-item>

View File

@@ -1,5 +1,8 @@
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 { 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.preventDefault()
this.launcherService.launch(this.pkg.entry)
this.launcherService.launch(addressInfo)
}
}

View File

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

View File

@@ -3,10 +3,13 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
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 { 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 { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component'
import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component'
@@ -51,7 +54,6 @@ const routes: Routes = [
RouterModule.forChild(routes),
AppConfigPageModule,
EmverPipesModule,
LaunchablePipeModule,
UiPipeModule,
ResponsiveColModule,
SharedPipesModule,

View File

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

View File

@@ -35,10 +35,16 @@ export class AppShowAdditionalComponent {
}
async presentModalLicense() {
const { id, version } = this.pkg.manifest
const modal = await this.modalCtrl.create({
componentProps: {
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,
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -180,7 +180,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
},
'marketing-site': {
type: 'string',
name: 'Marketing Site',
name: 'Website',
description: 'URL to the marketing site / channel for the project',
placeholder: 'e.g. start9.com',
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.state === PackageState.Installed">
<ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === -1"
expand="block"
color="success"
(click)="tryInstall()"
@@ -18,7 +18,7 @@
Update
</ion-button>
<ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 1"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
@@ -27,7 +27,7 @@
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 0"
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 0"
expand="block"
color="success"
(click)="tryInstall()"

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,13 @@ import { SideloadPage } from './sideload.page'
import { Routes, RouterModule } from '@angular/router'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
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 = [
{
@@ -20,6 +27,11 @@ const routes: Routes = [
RouterModule.forChild(routes),
SharedPipesModule,
EmverPipesModule,
PackageModule,
AboutModule,
AdditionalModule,
DependenciesModule,
MarketplaceShowComponentsModule,
],
declarations: [SideloadPage, DragNDropDirective],
})

View File

@@ -7,92 +7,70 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-text-center with-widgets">
<!-- file upload -->
<div
*ngIf="!toUpload.file; else fileUploaded"
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>
<ion-content class="ion-padding with-widgets">
<!-- invalid -->
<div *ngIf="invalid; else valid" class="drop-area_filled">
<h4>
<ion-icon
name="close-circle-outline"
color="danger"
class="inline"
></ion-icon>
Invalid package file
</h4>
<ion-button color="primary" (click)="clear()">Try again</ion-button>
</div>
<!-- file uploaded -->
<ng-template #fileUploaded>
<div class="drop-area_filled">
<h4>
<ion-icon
*ngIf="uploadState?.invalid"
name="close-circle-outline"
color="danger"
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
<!-- valid -->
<ng-template #valid>
<!-- uploaded -->
<div class="ion-padding" *ngIf="pkgData?.pkg as pkg; else empty">
<div class="ion-text-right">
<ion-button color="danger" (click)="clear()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</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>
<!-- 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>
</ion-content>

View File

@@ -2,11 +2,6 @@
vertical-align: initial;
}
.area {
flex-direction: column;
justify-content: center;
}
.drop-area {
display: flex;
background-color: rgba(24, 24, 24, 0.5);
@@ -18,7 +13,7 @@
border-color: var(--ion-color-dark);
color: var(--ion-color-dark);
border-radius: 5px;
margin: 60px;
margin: 20px;
padding: 30px;
min-height: 600px;
@@ -47,45 +42,3 @@
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 { isPlatform, LoadingController, NavController } from '@ionic/angular'
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 cbor from 'cbor'
import { ErrorToastService } from '@start9labs/shared'
import cbor from 'cbor'
interface Positions {
[key: string]: [bigint, bigint] // [position, length]
@@ -20,20 +20,12 @@ const VERSION = new Uint8Array([1])
})
export class SideloadPage {
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
toUpload: {
manifest: Manifest | null
icon: string | null
file: File | null
} = {
manifest: null,
icon: null,
file: null,
pkgData?: {
pkg: MarketplacePkg
file: File
}
onTor = this.config.isTor()
uploadState?: {
invalid: boolean
message: string
}
invalid = false
constructor(
private readonly loadingCtrl: LoadingController,
@@ -53,63 +45,60 @@ export class SideloadPage {
this.setFile(files)
}
async setFile(files?: File[]) {
if (!files || !files.length) return
const file = files[0]
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
clear() {
this.pkgData = undefined
this.invalid = false
}
async handleUpload() {
if (!this.pkgData) return
const loader = await this.loadingCtrl.create({
message: 'Uploading package',
cssClass: 'loader',
})
await loader.present()
const { pkg, file } = this.pkgData
try {
const guid = await this.api.sideloadPackage({
manifest: this.toUpload.manifest!,
icon: this.toUpload.icon!,
size: this.toUpload.file!.size,
manifest: pkg.manifest,
icon: pkg.icon,
size: file.size,
})
this.api
.uploadPackage(guid, this.toUpload.file!)
.catch(e => console.error(e))
this.api.uploadPackage(guid, file).catch(e => console.error(e))
this.navCtrl.navigateRoot('/services')
} catch (e: any) {
this.errToast.present(e)
} finally {
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 = {}
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
let start = 103
@@ -119,30 +108,51 @@ export class SideloadPage {
).getUint32(0, false)
await getPositions(start, end, file, positions, tocLength as any)
await this.getManifest(positions, file)
await this.getIcon(positions, file)
const manifest = await this.getAsset(positions, file, 'manifest')
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(
file.slice(
Number(positions['manifest'][0]),
Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
Number(positions[asset][0]),
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) {
const contentType = `image/${this.toUpload.manifest?.assets.icon
.split('.')
.pop()}`
private async getIcon(
positions: Positions,
file: Blob,
manifest: Manifest,
): Promise<string> {
const contentType = `image/${manifest.assets.icon.split('.').pop()}`
const data = file.slice(
Number(positions['icon'][0]),
Number(positions['icon'][0]) + Number(positions['icon'][1]),
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 {
AbstractMarketplaceService,
Marketplace,
MarketplaceManifest,
Manifest,
MarketplacePkg,
StoreIdentity,
} from '@start9labs/marketplace'
@@ -64,7 +64,7 @@ export class UpdatesPage {
}
async tryUpdate(
manifest: MarketplaceManifest,
manifest: Manifest,
url: string,
local: PackageDataEntry,
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({
message: 'Checking dependent services...',
})
@@ -181,7 +181,7 @@ export class FilterUpdatesPipe implements PipeTransform {
({ manifest }) =>
this.emver.compare(
manifest.version,
local[manifest.id]?.installed?.manifest.version || '',
local[manifest.id]?.manifest.version || '', // @TODO this won't work, need old version
) === 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 { InterfaceDef } from '../../services/patch-db/data-model'
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
import { hasUi } from '../../services/config.service'
@Pipe({
name: 'hasUi',
})
export class UiPipe implements PipeTransform {
transform(interfaces: Record<string, InterfaceDef>): boolean {
return hasUi(interfaces)
transform(addressInfo: InstalledPackageInfo['address-info']): boolean {
return hasUi(addressInfo)
}
}

View File

@@ -1,7 +1,5 @@
import {
DependencyErrorType,
DockerIoFormat,
Manifest,
PackageDataEntry,
PackageMainStatus,
PackageState,
@@ -9,7 +7,11 @@ import {
} from 'src/app/services/patch-db/data-model'
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types'
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'
export module Mock {
@@ -17,6 +19,7 @@ export module Mock {
'backup-progress': null,
'update-progress': null,
updated: true,
'shutting-down': false,
}
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
version: '0.3.4.3',
@@ -50,16 +53,11 @@ export module Mock {
short: 'A Bitcoin full node by Bitcoin Core.',
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
},
replaces: ['banks', 'governments'],
'release-notes': 'Taproot, Schnorr, and more.',
assets: {
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',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
@@ -74,299 +72,8 @@ export module Mock {
start: 'Starting Bitcoin is good for your health.',
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: {},
'os-version': '0.4.0',
}
export const MockManifestLnd: Manifest = {
@@ -377,15 +84,10 @@ export module Mock {
short: 'A bolt spec compliant client.',
long: 'More info about LND. More info about LND. More info about LND.',
},
'release-notes': 'Dual funded channels!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
'release-notes': 'Dual funded channels!',
license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -400,104 +102,6 @@ export module Mock {
start: 'Starting LND is good for your health.',
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: {
bitcoind: {
version: '=0.21.0',
@@ -506,7 +110,6 @@ export module Mock {
type: 'opt-out',
how: 'You can use an external node from your server if you prefer.',
},
config: null,
},
'btc-rpc-proxy': {
version: '>=0.2.2',
@@ -516,9 +119,9 @@ export module Mock {
type: 'opt-in',
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 = {
@@ -530,15 +133,10 @@ export module Mock {
short: 'A super charger for your Bitcoin node.',
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: {
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',
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -552,66 +150,6 @@ export module Mock {
start: 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: {
bitcoind: {
version: '>=0.20.0',
@@ -619,34 +157,9 @@ export module Mock {
requirement: {
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 = {
@@ -1993,14 +1506,9 @@ export module Mock {
export const bitcoind: PackageDataEntry = {
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,
icon: '/assets/img/service-icons/bitcoind.png',
installed: {
manifest: MockManifestBitcoind,
'last-backup': null,
status: {
configured: true,
@@ -2011,35 +1519,62 @@ export module Mock {
},
'dependency-errors': {},
},
'interface-addresses': {
ui: {
'tor-address': 'bitcoind-ui-address.onion',
'lan-address': 'bitcoind-ui-address.local',
},
'address-info': {
rpc: {
'tor-address': 'bitcoind-rpc-address.onion',
'lan-address': 'bitcoind-rpc-address.local',
name: 'Bitcoin RPC',
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: {
'tor-address': 'bitcoind-p2p-address.onion',
'lan-address': 'bitcoind-p2p-address.local',
name: 'Bitcoin P2P',
description: `Bitcoin's P2P interface`,
addresses: [
'bitcoin://bitcoind-rpc-address.onion',
'bitcoin://192.168.1.1:8333',
],
ui: true,
},
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/',
'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 = {
state: PackageState.Installed,
'static-files': {
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',
},
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
manifest: MockManifestBitcoinProxy,
installed: {
'last-backup': null,
@@ -2050,11 +1585,15 @@ export module Mock {
},
'dependency-errors': {},
},
manifest: MockManifestBitcoinProxy,
'interface-addresses': {
'address-info': {
rpc: {
'tor-address': 'bitcoinproxy-rpc-address.onion',
'lan-address': 'bitcoinproxy-rpc-address.local',
name: 'Proxy RPC addresses',
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': {
@@ -2064,23 +1603,20 @@ export module Mock {
},
'dependency-info': {
bitcoind: {
manifest: Mock.MockManifestBitcoind,
title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg',
},
},
'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key',
'has-config': true,
},
'install-progress': undefined,
actions: {},
}
export const lnd: PackageDataEntry = {
state: PackageState.Installed,
'static-files': {
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',
},
icon: '/assets/img/service-icons/lnd.png',
manifest: MockManifestLnd,
installed: {
'last-backup': null,
@@ -2095,15 +1631,26 @@ export module Mock {
},
},
},
manifest: MockManifestLnd,
'interface-addresses': {
rpc: {
'tor-address': 'lnd-rpc-address.onion',
'lan-address': 'lnd-rpc-address.local',
'address-info': {
ui: {
name: 'Web UI',
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: {
'tor-address': 'lnd-grpc-address.onion',
'lan-address': 'lnd-grpc-address.local',
name: 'gRPC',
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': {
@@ -2116,23 +1663,24 @@ export module Mock {
},
'dependency-info': {
bitcoind: {
manifest: Mock.MockManifestBitcoind,
title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg',
},
'btc-rpc-proxy': {
manifest: Mock.MockManifestBitcoinProxy,
title: 'Bitcoin Proxy',
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
},
},
'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key',
'has-config': true,
},
'install-progress': undefined,
actions: {},
}
export const LocalPkgs: { [key: string]: PackageDataEntry } = {
bitcoind: bitcoind,
bitcoind,
'btc-rpc-proxy': bitcoinProxy,
lnd: lnd,
lnd,
}
}

View File

@@ -1,11 +1,10 @@
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 { InputSpec } from 'start-sdk/types/config-types'
import {
DataModel,
DependencyError,
Manifest,
} from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'

View File

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

View File

@@ -1,13 +1,10 @@
import {
DataModel,
DependencyErrorType,
DockerIoFormat,
HealthResult,
Manifest,
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { Mock } from './api.fixures'
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
export const mockPatchData: DataModel = {
@@ -68,6 +65,7 @@ export const mockPatchData: DataModel = {
'backup-progress': null,
updated: false,
'update-progress': null,
'shutting-down': false,
},
hostname: 'random-words',
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
@@ -78,11 +76,7 @@ export const mockPatchData: DataModel = {
'package-data': {
bitcoind: {
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',
},
icon: '/assets/img/service-icons/bitcoind.svg',
manifest: {
id: 'bitcoind',
title: 'Bitcoin Core',
@@ -92,15 +86,10 @@ export const mockPatchData: DataModel = {
short: 'A Bitcoin full node by Bitcoin Core.',
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
},
'release-notes': 'Taproot, Schnorr, and more.',
assets: {
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',
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
@@ -115,308 +104,10 @@ export const mockPatchData: DataModel = {
start: 'Starting Bitcoin is good for your health.',
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: {},
'os-version': '0.4.0',
},
installed: {
manifest: {
...Mock.MockManifestBitcoind,
version: '0.20.0',
},
'last-backup': null,
status: {
configured: true,
@@ -446,38 +137,43 @@ export const mockPatchData: DataModel = {
'unnecessary-health-check': {
name: 'Totally Unnecessary',
result: HealthResult.Disabled,
reason: 'You disabled this on purpose',
},
},
},
'dependency-errors': {},
},
'interface-addresses': {
ui: {
'tor-address': 'bitcoind-ui-address.onion',
'lan-address': 'bitcoind-ui-address.local',
},
'address-info': {
rpc: {
'tor-address': 'bitcoind-rpc-address.onion',
'lan-address': 'bitcoind-rpc-address.local',
name: 'Bitcoin RPC',
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: {
'tor-address': 'bitcoind-p2p-address.onion',
'lan-address': 'bitcoind-p2p-address.local',
name: 'Bitcoin P2P',
description: `Bitcoin's P2P interface`,
addresses: [
'bitcoin://bitcoind-rpc-address.onion',
'bitcoin://192.168.1.1:8333',
],
ui: true,
},
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key',
'has-config': true,
},
},
lnd: {
state: PackageState.Installed,
'static-files': {
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',
},
icon: '/assets/img/service-icons/lnd.png',
manifest: {
id: 'lnd',
title: 'Lightning Network Daemon',
@@ -486,15 +182,10 @@ export const mockPatchData: DataModel = {
short: 'A bolt spec compliant client.',
long: 'More info about LND. More info about LND. More info about LND.',
},
'release-notes': 'Dual funded channels!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
'release-notes': 'Dual funded channels!',
license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -509,104 +200,6 @@ export const mockPatchData: DataModel = {
start: 'Starting LND is good for your health.',
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: {
bitcoind: {
version: '=0.21.0',
@@ -615,7 +208,6 @@ export const mockPatchData: DataModel = {
type: 'opt-out',
how: 'You can use an external node from your server if you prefer.',
},
config: null,
},
'btc-rpc-proxy': {
version: '>=0.2.2',
@@ -625,9 +217,9 @@ export const mockPatchData: DataModel = {
type: 'opt-in',
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: {
manifest: {
@@ -647,14 +239,26 @@ export const mockPatchData: DataModel = {
},
},
},
'interface-addresses': {
rpc: {
'tor-address': 'lnd-rpc-address.onion',
'lan-address': 'lnd-rpc-address.local',
'address-info': {
ui: {
name: 'Web UI',
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: {
'tor-address': 'lnd-grpc-address.onion',
'lan-address': 'lnd-grpc-address.local',
name: 'gRPC',
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': {
@@ -667,20 +271,17 @@ export const mockPatchData: DataModel = {
},
'dependency-info': {
bitcoind: {
manifest: {
title: 'Bitcoin Core',
} as Manifest,
title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg',
},
'btc-rpc-proxy': {
manifest: {
title: 'Bitcoin Proxy',
} as Manifest,
title: 'Bitcoin Proxy',
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
},
},
'marketplace-url': 'https://registry.start9.com/',
'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 { WorkspaceConfig } from '@start9labs/shared'
import {
InterfaceDef,
PackageDataEntry,
InstalledPackageInfo,
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
const {
@@ -52,56 +50,12 @@ export class ConfigService {
isSecure(): boolean {
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 {
const int = getUiInterfaceValue(interfaces)
return !!int?.['tor-config']
}
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 hasUi(
addressInfo: InstalledPackageInfo['address-info'],
): boolean {
return !!Object.values(addressInfo).find(a => a.ui)
}
export function removeProtocol(str: string): string {
@@ -113,15 +67,3 @@ export function removeProtocol(str: string): string {
export function removePort(str: string): string {
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 { 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'
export interface DataModel {
@@ -11,7 +11,7 @@ export interface DataModel {
export interface UIData {
name: string | null
'ack-welcome': string // eOS emver
'ack-welcome': string // emver
marketplace: UIMarketplaceData
dev: DevData
gaming: {
@@ -95,176 +95,98 @@ export interface ServerStatusInfo {
}
updated: boolean
'update-progress': { size: number | null; downloaded: number } | null
}
export enum ServerStatus {
Running = 'running',
Updated = 'updated',
BackingUp = 'backing-up',
'shutting-down': boolean
}
export interface PackageDataEntry {
state: PackageState
'static-files': {
license: Url
instructions: Url
icon: Url
}
manifest: Manifest
installed?: InstalledPackageDataEntry // exists when: installed, updating
'install-progress'?: InstallProgress // exists when: installing, updating
icon: string
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 {
Installing = 'installing',
Installed = 'installed',
Updating = 'updating',
Removing = 'removing',
Restoring = 'restoring',
NeedsUpdate = 'needs-update',
}
export interface InstalledPackageDataEntry {
export interface InstalledPackageInfo {
status: Status
manifest: Manifest
'last-backup': string | null
'current-dependencies': { [id: string]: CurrentDependencyInfo }
'dependency-info': {
[id: string]: {
manifest: Manifest
icon: Url
}
}
'interface-addresses': {
[id: string]: { 'tor-address': string; 'lan-address': string }
}
'current-dependencies': Record<string, CurrentDependencyInfo>
'dependency-info': Record<string, { title: string; icon: Url }>
'address-info': Record<string, AddressInfo>
'marketplace-url': string | null
'developer-key': string
'has-config': boolean
}
export interface CurrentDependencyInfo {
'health-checks': string[] // array of health check IDs
}
export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
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 {
export interface AddressInfo {
name: string
description: string
'tor-config': TorConfig | null
'lan-config': LanConfig | null
addresses: Url[]
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 {
name: string
description: string
warning: string | null
implementation: ActionImpl
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
disabled: string | null
'input-spec': InputSpec | null
group: string | null
}
export interface Status {
@@ -280,6 +202,7 @@ export type MainStatus =
| MainStatusRunning
| MainStatusBackingUp
| MainStatusRestarting
| MainStatusConfiguring
export interface MainStatusStopped {
status: PackageMainStatus.Stopped
@@ -301,13 +224,16 @@ export interface MainStatusRunning {
export interface MainStatusBackingUp {
status: PackageMainStatus.BackingUp
started: string | null // UTC date string
}
export interface MainStatusRestarting {
status: PackageMainStatus.Restarting
}
export interface MainStatusConfiguring {
status: PackageMainStatus.Configuring
}
export enum PackageMainStatus {
Starting = 'starting',
Running = 'running',
@@ -315,6 +241,7 @@ export enum PackageMainStatus {
Stopped = 'stopped',
BackingUp = 'backing-up',
Restarting = 'restarting',
Configuring = 'configuring',
}
export type HealthCheckResult = { name: string } & (
@@ -339,6 +266,7 @@ export interface HealthCheckResultStarting {
export interface HealthCheckResultDisabled {
result: HealthResult.Disabled
reason: string
}
export interface HealthCheckResultSuccess {
@@ -370,7 +298,6 @@ export enum DependencyErrorType {
IncorrectVersion = 'incorrect-version',
ConfigUnsatisfied = 'config-unsatisfied',
HealthChecksFailed = 'health-checks-failed',
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
Transitive = 'transitive',
}

View File

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

View File

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