mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
update FE types and unify sideload page with marketplace show
This commit is contained in:
committed by
Aiden McClelland
parent
6556fcc531
commit
cb7790ccba
@@ -1,4 +0,0 @@
|
||||
.all-notes {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -53,3 +53,5 @@ export function toUrl(text: string | null | undefined): string {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export type WithId<T> = T & { id: string }
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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$
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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()"
|
||||
@@ -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.
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { LaunchablePipe } from './launchable.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [LaunchablePipe],
|
||||
exports: [LaunchablePipe],
|
||||
})
|
||||
export class LaunchablePipeModule {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user