mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +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">
|
<div class="text">
|
||||||
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
|
<h1 ticker class="title">{{ pkg.manifest.title }}</h1>
|
||||||
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
|
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
|
||||||
<p class="published">
|
<p *ngIf="pkg['published-at'] as published" class="published">
|
||||||
Released: {{ pkg['published-at'] | date: 'medium' }}
|
Released: {{ published | date : 'medium' }}
|
||||||
</p>
|
</p>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface MarketplacePkg {
|
|||||||
icon: Url
|
icon: Url
|
||||||
license: Url
|
license: Url
|
||||||
instructions: Url
|
instructions: Url
|
||||||
manifest: MarketplaceManifest
|
manifest: Manifest
|
||||||
categories: string[]
|
categories: string[]
|
||||||
versions: string[]
|
versions: string[]
|
||||||
'dependency-metadata': {
|
'dependency-metadata': {
|
||||||
@@ -38,7 +38,7 @@ export interface DependencyMetadata {
|
|||||||
hidden: boolean
|
hidden: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketplaceManifest<T = unknown> {
|
export interface Manifest {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
version: string
|
version: string
|
||||||
@@ -52,7 +52,7 @@ export interface MarketplaceManifest<T = unknown> {
|
|||||||
}
|
}
|
||||||
replaces?: string[]
|
replaces?: string[]
|
||||||
'release-notes': string
|
'release-notes': string
|
||||||
license: string // type of license
|
license: string // name of license
|
||||||
'wrapper-repo': Url
|
'wrapper-repo': Url
|
||||||
'upstream-repo': Url
|
'upstream-repo': Url
|
||||||
'support-site': Url
|
'support-site': Url
|
||||||
@@ -65,10 +65,11 @@ export interface MarketplaceManifest<T = unknown> {
|
|||||||
start: string | null
|
start: string | null
|
||||||
stop: string | null
|
stop: string | null
|
||||||
}
|
}
|
||||||
dependencies: Record<string, Dependency<T>>
|
dependencies: Record<string, Dependency>
|
||||||
|
'os-version': string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Dependency<T> {
|
export interface Dependency {
|
||||||
version: string
|
version: string
|
||||||
requirement:
|
requirement:
|
||||||
| {
|
| {
|
||||||
@@ -83,5 +84,4 @@ export interface Dependency<T> {
|
|||||||
type: 'required'
|
type: 'required'
|
||||||
}
|
}
|
||||||
description: string | null
|
description: string | null
|
||||||
config: T
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,3 +53,5 @@ export function toUrl(text: string | null | undefined): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WithId<T> = T & { id: string }
|
||||||
|
|||||||
@@ -94,10 +94,7 @@ export class MenuComponent {
|
|||||||
Object.entries(marketplace).reduce((list, [_, store]) => {
|
Object.entries(marketplace).reduce((list, [_, store]) => {
|
||||||
store?.packages.forEach(({ manifest: { id, version } }) => {
|
store?.packages.forEach(({ manifest: { id, version } }) => {
|
||||||
if (
|
if (
|
||||||
this.emver.compare(
|
this.emver.compare(version, local[id]?.manifest.version || '') === 1
|
||||||
version,
|
|
||||||
local[id]?.installed?.manifest.version || '',
|
|
||||||
) === 1
|
|
||||||
)
|
)
|
||||||
list.add(id)
|
list.add(id)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { RouterModule } from '@angular/router'
|
import { RouterModule } from '@angular/router'
|
||||||
|
|
||||||
import { AnyLinkComponent } from './any-link.component'
|
import { AnyLinkComponent } from './any-link.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -9,13 +9,6 @@
|
|||||||
<span *ngIf="!installProgress">
|
<span *ngIf="!installProgress">
|
||||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||||
<span
|
|
||||||
*ngIf="
|
|
||||||
rendering.display === PR[PS.Stopping].display &&
|
|
||||||
(sigtermTimeout | durationToSeconds) > 30
|
|
||||||
"
|
|
||||||
>this may take a while</span
|
|
||||||
>
|
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="installProgress">
|
<span *ngIf="installProgress">
|
||||||
<ion-text
|
<ion-text
|
||||||
@@ -23,7 +16,8 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
Installing
|
Installing
|
||||||
<span class="loading-dots"></span>{{ progress }}
|
<span class="loading-dots"></span>
|
||||||
|
{{ progress }}
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export class StatusComponent {
|
|||||||
@Input() style?: string = 'regular'
|
@Input() style?: string = 'regular'
|
||||||
@Input() weight?: string = 'normal'
|
@Input() weight?: string = 'normal'
|
||||||
@Input() installProgress?: InstallProgress
|
@Input() installProgress?: InstallProgress
|
||||||
@Input() sigtermTimeout?: string | null = null
|
|
||||||
|
|
||||||
readonly connected$ = this.connectionService.connected$
|
readonly connected$ = this.connectionService.connected$
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<ng-template #notLoading>
|
<ng-template #notLoading>
|
||||||
<ion-item *ngIf="loadingError; else noError">
|
<ion-item *ngIf="loadingError; else noError">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<ion-text color="danger"> {{ loadingError }} </ion-text>
|
<ion-text color="danger">{{ loadingError }}</ion-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
@@ -59,13 +59,14 @@
|
|||||||
<h2 style="display: flex; align-items: center">
|
<h2 style="display: flex; align-items: center">
|
||||||
<img
|
<img
|
||||||
style="width: 18px; margin: 4px"
|
style="width: 18px; margin: 4px"
|
||||||
[src]="pkg['static-files'].icon"
|
[src]="pkg.icon"
|
||||||
[alt]="pkg.manifest.title"
|
[alt]="pkg.manifest.title"
|
||||||
/>
|
/>
|
||||||
<ion-text
|
<ion-text
|
||||||
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
|
style="margin: 5px; font-family: 'Montserrat'; font-size: 18px"
|
||||||
>{{ pkg.manifest.title }}</ion-text
|
|
||||||
>
|
>
|
||||||
|
{{ pkg.manifest.title }}
|
||||||
|
</ion-text>
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
<ion-text color="dark">
|
<ion-text color="dark">
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<!-- no options -->
|
<!-- no options -->
|
||||||
<ion-item *ngIf="!hasOptions">
|
<ion-item *ngIf="!pkg.installed?.['has-config']">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p>
|
<p>
|
||||||
No config options for {{ pkg.manifest.title }} {{
|
No config options for {{ pkg.manifest.title }} {{
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ng-container *ngIf="!loading && !loadingError">
|
<ng-container *ngIf="!loading && !loadingError">
|
||||||
<ion-buttons
|
<ion-buttons
|
||||||
*ngIf="configForm && hasOptions"
|
*ngIf="configForm && pkg.installed?.['has-config']"
|
||||||
slot="start"
|
slot="start"
|
||||||
class="ion-padding-start"
|
class="ion-padding-start"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { InputSpec } from 'start-sdk/types/config-types'
|
|||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { UntypedFormGroup } from '@angular/forms'
|
import { UntypedFormGroup } from '@angular/forms'
|
||||||
@@ -36,10 +37,10 @@ import { Breakages } from 'src/app/services/api/api.types'
|
|||||||
})
|
})
|
||||||
export class AppConfigPage {
|
export class AppConfigPage {
|
||||||
@Input() pkgId!: string
|
@Input() pkgId!: string
|
||||||
|
|
||||||
@Input() dependentInfo?: DependentInfo
|
@Input() dependentInfo?: DependentInfo
|
||||||
|
|
||||||
pkg!: PackageDataEntry
|
pkg!: PackageDataEntry
|
||||||
|
|
||||||
loadingText = ''
|
loadingText = ''
|
||||||
|
|
||||||
configSpec?: InputSpec
|
configSpec?: InputSpec
|
||||||
@@ -53,8 +54,6 @@ export class AppConfigPage {
|
|||||||
saving = false
|
saving = false
|
||||||
loadingError: string | IonicSafeString = ''
|
loadingError: string | IonicSafeString = ''
|
||||||
|
|
||||||
hasOptions = false
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly embassyApi: ApiService,
|
private readonly embassyApi: ApiService,
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly errToast: ErrorToastService,
|
||||||
@@ -68,10 +67,9 @@ export class AppConfigPage {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
const pkg = await getPackage(this.patch, this.pkgId)
|
const pkg = await getPackage(this.patch, this.pkgId)
|
||||||
if (!pkg) return
|
if (!pkg?.installed?.['has-config']) return
|
||||||
this.pkg = pkg
|
|
||||||
|
|
||||||
if (!this.pkg.manifest.config) return
|
this.pkg = pkg
|
||||||
|
|
||||||
let newConfig: object | undefined
|
let newConfig: object | undefined
|
||||||
let patch: Operation[] | undefined
|
let patch: Operation[] | undefined
|
||||||
@@ -104,8 +102,6 @@ export class AppConfigPage {
|
|||||||
newConfig || this.original,
|
newConfig || this.original,
|
||||||
)
|
)
|
||||||
|
|
||||||
this.hasOptions = false
|
|
||||||
|
|
||||||
if (patch) {
|
if (patch) {
|
||||||
this.diff = this.getDiff(patch)
|
this.diff = this.getDiff(patch)
|
||||||
this.markDirty(patch)
|
this.markDirty(patch)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { ModalController } from '@ionic/angular'
|
import { ModalController } from '@ionic/angular'
|
||||||
import { map, take } from 'rxjs/operators'
|
import { map } from 'rxjs/operators'
|
||||||
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
import { DataModel, PackageState } from 'src/app/services/patch-db/data-model'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { firstValueFrom } from 'rxjs'
|
||||||
@@ -36,7 +36,7 @@ export class BackupSelectPage {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
icon: pkg['static-files'].icon,
|
icon: pkg.icon,
|
||||||
disabled: pkg.state !== PackageState.Installed,
|
disabled: pkg.state !== PackageState.Installed,
|
||||||
checked: pkg.state === PackageState.Installed,
|
checked: pkg.state === PackageState.Installed,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { NgModule } from '@angular/core'
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { AppActionsPage, AppActionsItemComponent } from './app-actions.page'
|
import {
|
||||||
|
AppActionsPage,
|
||||||
|
AppActionsItemComponent,
|
||||||
|
GroupActionsPipe,
|
||||||
|
} from './app-actions.page'
|
||||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
|
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
|
||||||
@@ -25,6 +29,6 @@ const routes: Routes = [
|
|||||||
GenericFormPageModule,
|
GenericFormPageModule,
|
||||||
ActionSuccessPageModule,
|
ActionSuccessPageModule,
|
||||||
],
|
],
|
||||||
declarations: [AppActionsPage, AppActionsItemComponent],
|
declarations: [AppActionsPage, AppActionsItemComponent, GroupActionsPipe],
|
||||||
})
|
})
|
||||||
export class AppActionsPageModule {}
|
export class AppActionsPageModule {}
|
||||||
|
|||||||
@@ -21,17 +21,19 @@
|
|||||||
></app-actions-item>
|
></app-actions-item>
|
||||||
|
|
||||||
<!-- ** specific actions ** -->
|
<!-- ** specific actions ** -->
|
||||||
<ion-item-divider *ngIf="!(pkg.manifest.actions | empty)">
|
<ng-container *ngIf="pkg.actions | groupActions as actionGroups">
|
||||||
Actions for {{ pkg.manifest.title }}
|
<ion-item-divider>Actions for {{ pkg.manifest.title }}</ion-item-divider>
|
||||||
</ion-item-divider>
|
<div *ngFor="let group of actionGroups" class="ion-padding-bottm">
|
||||||
<app-actions-item
|
<app-actions-item
|
||||||
*ngFor="let action of pkg.manifest.actions | keyvalue: asIsOrder"
|
*ngFor="let action of group"
|
||||||
[action]="{
|
[action]="{
|
||||||
name: action.value.name,
|
name: action.name,
|
||||||
description: action.value.description,
|
description: action.description,
|
||||||
icon: 'play-circle-outline'
|
icon: 'play-circle-outline'
|
||||||
}"
|
}"
|
||||||
(click)="handleAction(pkg, action)"
|
(click)="handleAction(action)"
|
||||||
></app-actions-item>
|
></app-actions-item>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
Pipe,
|
||||||
|
PipeTransform,
|
||||||
|
} from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import {
|
import {
|
||||||
@@ -12,12 +18,18 @@ import {
|
|||||||
Action,
|
Action,
|
||||||
DataModel,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||||
import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared'
|
import {
|
||||||
|
isEmptyObject,
|
||||||
|
ErrorToastService,
|
||||||
|
getPkgId,
|
||||||
|
WithId,
|
||||||
|
} from '@start9labs/shared'
|
||||||
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||||
|
import { filter } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-actions',
|
selector: 'app-actions',
|
||||||
@@ -27,7 +39,9 @@ import { hasCurrentDeps } from 'src/app/util/has-deps'
|
|||||||
})
|
})
|
||||||
export class AppActionsPage {
|
export class AppActionsPage {
|
||||||
readonly pkgId = getPkgId(this.route)
|
readonly pkgId = getPkgId(this.route)
|
||||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId)
|
readonly pkg$ = this.patch
|
||||||
|
.watch$('package-data', this.pkgId)
|
||||||
|
.pipe(filter(pkg => pkg.state === PackageState.Installed))
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
@@ -40,28 +54,27 @@ export class AppActionsPage {
|
|||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleAction(
|
async handleAction(action: WithId<Action>) {
|
||||||
pkg: PackageDataEntry,
|
if (action.disabled) {
|
||||||
action: { key: string; value: Action },
|
const alert = await this.alertCtrl.create({
|
||||||
) {
|
header: 'Forbidden',
|
||||||
const status = pkg.installed?.status
|
message: action.disabled,
|
||||||
if (
|
buttons: ['OK'],
|
||||||
status &&
|
cssClass: 'alert-error-message enter-click',
|
||||||
(action.value['allowed-statuses'] as PackageMainStatus[]).includes(
|
})
|
||||||
status.main.status,
|
await alert.present()
|
||||||
)
|
} else {
|
||||||
) {
|
if (!isEmptyObject(action['input-spec'] || {})) {
|
||||||
if (!isEmptyObject(action.value['input-spec'] || {})) {
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
component: GenericFormPage,
|
component: GenericFormPage,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: action.value.name,
|
title: action.name,
|
||||||
spec: action.value['input-spec'],
|
spec: action['input-spec'],
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Execute',
|
text: 'Execute',
|
||||||
handler: (value: any) => {
|
handler: (value: any) => {
|
||||||
return this.executeAction(action.key, value)
|
return this.executeAction(action.id, value)
|
||||||
},
|
},
|
||||||
isSubmit: true,
|
isSubmit: true,
|
||||||
},
|
},
|
||||||
@@ -72,9 +85,9 @@ export class AppActionsPage {
|
|||||||
} else {
|
} else {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Confirm',
|
header: 'Confirm',
|
||||||
message: `Are you sure you want to execute action "${
|
message: `Are you sure you want to execute action "${action.name}"? ${
|
||||||
action.value.name
|
action.warning || ''
|
||||||
}"? ${action.value.warning || ''}`,
|
}`,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Cancel',
|
text: 'Cancel',
|
||||||
@@ -83,7 +96,7 @@ export class AppActionsPage {
|
|||||||
{
|
{
|
||||||
text: 'Execute',
|
text: 'Execute',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
this.executeAction(action.key)
|
this.executeAction(action.id)
|
||||||
},
|
},
|
||||||
cssClass: 'enter-click',
|
cssClass: 'enter-click',
|
||||||
},
|
},
|
||||||
@@ -91,31 +104,6 @@ export class AppActionsPage {
|
|||||||
})
|
})
|
||||||
await alert.present()
|
await alert.present()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const statuses = [...action.value['allowed-statuses']]
|
|
||||||
const last = statuses.pop()
|
|
||||||
let statusesStr = statuses.join(', ')
|
|
||||||
let error = ''
|
|
||||||
if (statuses.length) {
|
|
||||||
if (statuses.length > 1) {
|
|
||||||
// oxford comma
|
|
||||||
statusesStr += ','
|
|
||||||
}
|
|
||||||
statusesStr += ` or ${last}`
|
|
||||||
} else if (last) {
|
|
||||||
statusesStr = `${last}`
|
|
||||||
} else {
|
|
||||||
error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
|
||||||
}
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
header: 'Forbidden',
|
|
||||||
message:
|
|
||||||
error ||
|
|
||||||
`Action "${action.value.name}" can only be executed when service is ${statusesStr}`,
|
|
||||||
buttons: ['OK'],
|
|
||||||
cssClass: 'alert-error-message enter-click',
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,3 +212,31 @@ interface LocalAction {
|
|||||||
export class AppActionsItemComponent {
|
export class AppActionsItemComponent {
|
||||||
@Input() action!: LocalAction
|
@Input() action!: LocalAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'groupActions',
|
||||||
|
})
|
||||||
|
export class GroupActionsPipe implements PipeTransform {
|
||||||
|
transform(
|
||||||
|
actions: PackageDataEntry['actions'],
|
||||||
|
): Array<Array<WithId<Action>>> | null {
|
||||||
|
if (!actions) return null
|
||||||
|
const noGroup = 'noGroup'
|
||||||
|
const grouped = Object.entries(actions).reduce<
|
||||||
|
Record<string, WithId<Action>[]>
|
||||||
|
>((groups, [id, action]) => {
|
||||||
|
const actionWithId = { id, ...action }
|
||||||
|
const groupKey = action.group || noGroup
|
||||||
|
if (!groups[groupKey]) {
|
||||||
|
groups[groupKey] = [actionWithId]
|
||||||
|
} else {
|
||||||
|
groups[groupKey].push(actionWithId)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return Object.values(grouped).map(group =>
|
||||||
|
group.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,72 +1,34 @@
|
|||||||
<ion-item *ngIf="interface">
|
<ion-item>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
slot="start"
|
slot="start"
|
||||||
size="large"
|
size="large"
|
||||||
[name]="interface.def.ui ? 'desktop-outline' : 'terminal-outline'"
|
[name]="addressInfo.ui ? 'desktop-outline' : 'terminal-outline'"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h1>{{ interface.def.name }}</h1>
|
<h1>{{ addressInfo.name }}</h1>
|
||||||
<h2>{{ interface.def.description }}</h2>
|
<h2>{{ addressInfo.description }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<div *ngIf="interface" style="padding-left: 64px">
|
<div style="padding-left: 64px">
|
||||||
<!-- has tor -->
|
<ion-item *ngFor="let address of addressInfo.addresses">
|
||||||
<ion-item *ngIf="interface.addresses['tor-address'] as tor">
|
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>Tor Address</h2>
|
<h2>{{ address | addressType }}</h2>
|
||||||
<p>{{ tor }}</p>
|
<p>{{ address }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(tor)">
|
<ion-button *ngIf="addressInfo.ui" fill="clear" (click)="launch(address)">
|
||||||
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" (click)="showQR(tor)">
|
<ion-button fill="clear" (click)="showQR(address)">
|
||||||
<ion-icon
|
<ion-icon
|
||||||
size="small"
|
size="small"
|
||||||
slot="icon-only"
|
slot="icon-only"
|
||||||
name="qr-code-outline"
|
name="qr-code-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button fill="clear" (click)="copy(tor)">
|
<ion-button fill="clear" (click)="copy(address)">
|
||||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<!-- no tor -->
|
|
||||||
<ion-item *ngIf="!interface.addresses['tor-address']">
|
|
||||||
<ion-label>
|
|
||||||
<h2>Tor Address</h2>
|
|
||||||
<p>Service does not use a Tor Address</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- lan -->
|
|
||||||
<ion-item *ngIf="interface.addresses['lan-address'] as lan">
|
|
||||||
<ion-label>
|
|
||||||
<h2>LAN Address</h2>
|
|
||||||
<p>{{ lan }}</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button *ngIf="interface.def.ui" fill="clear" (click)="launch(lan)">
|
|
||||||
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" (click)="showQR(lan)">
|
|
||||||
<ion-icon
|
|
||||||
size="small"
|
|
||||||
slot="icon-only"
|
|
||||||
name="qr-code-outline"
|
|
||||||
></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button fill="clear" (click)="copy(lan)">
|
|
||||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-item>
|
|
||||||
<!-- no lan -->
|
|
||||||
<ion-item *ngIf="!interface.addresses['lan-address']">
|
|
||||||
<ion-label>
|
|
||||||
<h2>LAN Address</h2>
|
|
||||||
<p>N/A</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
import { SharedPipesModule } from '@start9labs/shared'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AppInterfacesItemComponent,
|
AppInterfacesItemComponent,
|
||||||
AppInterfacesPage,
|
AppInterfacesPage,
|
||||||
|
AddressTypePipe,
|
||||||
} from './app-interfaces.page'
|
} from './app-interfaces.page'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -23,6 +23,10 @@ const routes: Routes = [
|
|||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
],
|
],
|
||||||
declarations: [AppInterfacesPage, AppInterfacesItemComponent],
|
declarations: [
|
||||||
|
AppInterfacesPage,
|
||||||
|
AppInterfacesItemComponent,
|
||||||
|
AddressTypePipe,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppInterfacesPageModule {}
|
export class AppInterfacesPageModule {}
|
||||||
|
|||||||
@@ -9,18 +9,11 @@
|
|||||||
|
|
||||||
<ion-content class="ion-padding-top with-widgets">
|
<ion-content class="ion-padding-top with-widgets">
|
||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<!-- iff ui -->
|
<div
|
||||||
<ng-container *ngIf="ui">
|
*ngFor="let addressInfo of (addressInfo$ | async)"
|
||||||
<ion-item-divider>User Interface</ion-item-divider>
|
style="margin-bottom: 30px"
|
||||||
<app-interfaces-item [interface]="ui"></app-interfaces-item>
|
>
|
||||||
</ng-container>
|
<app-interfaces-item [addressInfo]="addressInfo"></app-interfaces-item>
|
||||||
|
</div>
|
||||||
<!-- other interface -->
|
|
||||||
<ng-container *ngIf="other.length">
|
|
||||||
<ion-item-divider>Machine Interfaces</ion-item-divider>
|
|
||||||
<div *ngFor="let interface of other" style="margin-bottom: 30px">
|
|
||||||
<app-interfaces-item [interface]="interface"></app-interfaces-item>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,80 +1,38 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
Pipe,
|
||||||
|
PipeTransform,
|
||||||
|
} from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ModalController, ToastController } from '@ionic/angular'
|
import { ModalController, ToastController } from '@ionic/angular'
|
||||||
import { getPkgId, copyToClipboard } from '@start9labs/shared'
|
import { getPkgId, copyToClipboard } from '@start9labs/shared'
|
||||||
import { getUiInterfaceKey } from 'src/app/services/config.service'
|
import { AddressInfo, DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import {
|
|
||||||
DataModel,
|
|
||||||
InstalledPackageDataEntry,
|
|
||||||
InterfaceDef,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
import { QRComponent } from 'src/app/components/qr/qr.component'
|
||||||
import { getPackage } from '../../../util/get-package-data'
|
import { map } from 'rxjs'
|
||||||
|
|
||||||
interface LocalInterface {
|
|
||||||
def: InterfaceDef
|
|
||||||
addresses: InstalledPackageDataEntry['interface-addresses'][string]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-interfaces',
|
selector: 'app-interfaces',
|
||||||
templateUrl: './app-interfaces.page.html',
|
templateUrl: './app-interfaces.page.html',
|
||||||
styleUrls: ['./app-interfaces.page.scss'],
|
styleUrls: ['./app-interfaces.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppInterfacesPage {
|
export class AppInterfacesPage {
|
||||||
ui?: LocalInterface
|
|
||||||
other: LocalInterface[] = []
|
|
||||||
readonly pkgId = getPkgId(this.route)
|
readonly pkgId = getPkgId(this.route)
|
||||||
|
readonly addressInfo$ = this.patch
|
||||||
|
.watch$('package-data', this.pkgId, 'installed', 'address-info')
|
||||||
|
.pipe(
|
||||||
|
map(addressInfo =>
|
||||||
|
Object.values(addressInfo).sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
const pkg = await getPackage(this.patch, this.pkgId)
|
|
||||||
if (!pkg) return
|
|
||||||
|
|
||||||
const interfaces = pkg.manifest.interfaces
|
|
||||||
const uiKey = getUiInterfaceKey(interfaces)
|
|
||||||
|
|
||||||
if (!pkg.installed) return
|
|
||||||
|
|
||||||
const addressesMap = pkg.installed['interface-addresses']
|
|
||||||
|
|
||||||
if (uiKey) {
|
|
||||||
const uiAddresses = addressesMap[uiKey]
|
|
||||||
this.ui = {
|
|
||||||
def: interfaces[uiKey],
|
|
||||||
addresses: {
|
|
||||||
'lan-address': uiAddresses['lan-address']
|
|
||||||
? 'https://' + uiAddresses['lan-address']
|
|
||||||
: '',
|
|
||||||
'tor-address': uiAddresses['tor-address']
|
|
||||||
? 'http://' + uiAddresses['tor-address']
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.other = Object.keys(interfaces)
|
|
||||||
.filter(key => key !== uiKey)
|
|
||||||
.map(key => {
|
|
||||||
const addresses = addressesMap[key]
|
|
||||||
return {
|
|
||||||
def: interfaces[key],
|
|
||||||
addresses: {
|
|
||||||
'lan-address': addresses['lan-address']
|
|
||||||
? 'https://' + addresses['lan-address']
|
|
||||||
: '',
|
|
||||||
'tor-address': addresses['tor-address']
|
|
||||||
? 'http://' + addresses['tor-address']
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -84,7 +42,7 @@ export class AppInterfacesPage {
|
|||||||
})
|
})
|
||||||
export class AppInterfacesItemComponent {
|
export class AppInterfacesItemComponent {
|
||||||
@Input()
|
@Input()
|
||||||
interface!: LocalInterface
|
addressInfo!: AddressInfo
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly toastCtrl: ToastController,
|
private readonly toastCtrl: ToastController,
|
||||||
@@ -122,3 +80,31 @@ export class AppInterfacesItemComponent {
|
|||||||
await toast.present()
|
await toast.present()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'addressType',
|
||||||
|
})
|
||||||
|
export class AddressTypePipe implements PipeTransform {
|
||||||
|
transform(address: string): string {
|
||||||
|
if (isValidIpv4(address)) return 'IPv4'
|
||||||
|
if (isValidIpv6(address)) return 'IPv6'
|
||||||
|
|
||||||
|
const hostname = new URL(address).hostname
|
||||||
|
if (hostname.endsWith('.onion')) return 'Tor'
|
||||||
|
if (hostname.endsWith('.local')) return 'Local'
|
||||||
|
|
||||||
|
return 'Custom'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIpv4(address: string): boolean {
|
||||||
|
const regexExp =
|
||||||
|
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||||
|
return regexExp.test(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIpv6(address: string): boolean {
|
||||||
|
const regexExp =
|
||||||
|
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi
|
||||||
|
return regexExp.test(address)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
>
|
>
|
||||||
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
|
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
|
||||||
<ion-thumbnail slot="start">
|
<ion-thumbnail slot="start">
|
||||||
<img alt="" [src]="pkg.entry['static-files'].icon" />
|
<img alt="" [src]="pkg.entry.icon" />
|
||||||
</ion-thumbnail>
|
</ion-thumbnail>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 ticker>{{ manifest.title }}</h2>
|
<h2 ticker>{{ manifest.title }}</h2>
|
||||||
@@ -17,17 +17,18 @@
|
|||||||
[installProgress]="pkg.entry['install-progress']"
|
[installProgress]="pkg.entry['install-progress']"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
size="small"
|
size="small"
|
||||||
[sigtermTimeout]="manifest.main['sigterm-timeout']"
|
|
||||||
></status>
|
></status>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button
|
<ng-container *ngIf="pkg.entry.installed as installed">
|
||||||
*ngIf="manifest.interfaces | hasUi"
|
<ion-button
|
||||||
slot="end"
|
*ngIf="installed['address-info'] | hasUi"
|
||||||
fill="clear"
|
slot="end"
|
||||||
color="primary"
|
fill="clear"
|
||||||
(click)="launchUi($event)"
|
color="primary"
|
||||||
[disabled]="!(pkg.entry.state | isLaunchable: status:manifest.interfaces)"
|
(click)="launchUi($event, installed['address-info'])"
|
||||||
>
|
[disabled]="status !== 'running'"
|
||||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
>
|
||||||
</ion-button>
|
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ng-container>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
import {
|
||||||
|
InstalledPackageInfo,
|
||||||
|
PackageMainStatus,
|
||||||
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { PkgInfo } from 'src/app/util/get-package-info'
|
import { PkgInfo } from 'src/app/util/get-package-info'
|
||||||
import { UiLauncherService } from 'src/app/services/ui-launcher.service'
|
import { UiLauncherService } from 'src/app/services/ui-launcher.service'
|
||||||
|
|
||||||
@@ -20,9 +23,9 @@ export class AppListPkgComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUi(e: Event): void {
|
launchUi(e: Event, addressInfo: InstalledPackageInfo['address-info']): void {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.launcherService.launch(this.pkg.entry)
|
this.launcherService.launch(addressInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||||
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
|
|
||||||
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
||||||
import { AppListIconComponent } from './app-list-icon/app-list-icon.component'
|
import { AppListIconComponent } from './app-list-icon/app-list-icon.component'
|
||||||
import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component'
|
import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component'
|
||||||
@@ -31,7 +30,6 @@ const routes: Routes = [
|
|||||||
StatusComponentModule,
|
StatusComponentModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
TextSpinnerComponentModule,
|
TextSpinnerComponentModule,
|
||||||
LaunchablePipeModule,
|
|
||||||
UiPipeModule,
|
UiPipeModule,
|
||||||
IonicModule,
|
IonicModule,
|
||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import { CommonModule } from '@angular/common'
|
|||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { IonicModule } from '@ionic/angular'
|
import { IonicModule } from '@ionic/angular'
|
||||||
import { AppShowPage } from './app-show.page'
|
import { AppShowPage } from './app-show.page'
|
||||||
import { EmverPipesModule, ResponsiveColModule, SharedPipesModule } from '@start9labs/shared'
|
import {
|
||||||
|
EmverPipesModule,
|
||||||
|
ResponsiveColModule,
|
||||||
|
SharedPipesModule,
|
||||||
|
} from '@start9labs/shared'
|
||||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||||
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module'
|
||||||
import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
|
|
||||||
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
||||||
import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component'
|
import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component'
|
||||||
import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component'
|
import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component'
|
||||||
@@ -51,7 +54,6 @@ const routes: Routes = [
|
|||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
AppConfigPageModule,
|
AppConfigPageModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
LaunchablePipeModule,
|
|
||||||
UiPipeModule,
|
UiPipeModule,
|
||||||
ResponsiveColModule,
|
ResponsiveColModule,
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
detail="false"
|
detail="false"
|
||||||
>
|
>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>Marketing Site</h2>
|
<h2>Website</h2>
|
||||||
<p>{{ manifest['marketing-site'] || 'Not provided' }}</p>
|
<p>{{ manifest['marketing-site'] || 'Not provided' }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||||
|
|||||||
@@ -35,10 +35,16 @@ export class AppShowAdditionalComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async presentModalLicense() {
|
async presentModalLicense() {
|
||||||
|
const { id, version } = this.pkg.manifest
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: 'License',
|
title: 'License',
|
||||||
content: from(this.api.getStatic(this.pkg['static-files']['license'])),
|
content: from(
|
||||||
|
this.api.getStatic(
|
||||||
|
`/public/package-data/${id}/${version}/LICENSE.md`,
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
component: MarkdownComponent,
|
component: MarkdownComponent,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<ion-back-button defaultHref="services"></ion-back-button>
|
<ion-back-button defaultHref="services"></ion-back-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img class="logo" [src]="pkg['static-files'].icon" alt="" />
|
<img class="logo" [src]="pkg.icon" alt="" />
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h1
|
<h1
|
||||||
class="montserrat"
|
class="montserrat"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
weight="600"
|
weight="600"
|
||||||
[installProgress]="pkg['install-progress']"
|
[installProgress]="pkg['install-progress']"
|
||||||
[rendering]="PR[status.primary]"
|
[rendering]="PR[status.primary]"
|
||||||
[sigtermTimeout]="pkg.manifest.main['sigterm-timeout']"
|
|
||||||
></status>
|
></status>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="isStopped && pkgStatus?.configured"
|
*ngIf="isStopped && isConfigured"
|
||||||
class="action-button"
|
class="action-button"
|
||||||
color="success"
|
color="success"
|
||||||
(click)="tryStart()"
|
(click)="tryStart()"
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="!pkgStatus?.configured"
|
*ngIf="!isConfigured"
|
||||||
class="action-button"
|
class="action-button"
|
||||||
color="warning"
|
color="warning"
|
||||||
(click)="presentModalConfig()"
|
(click)="presentModalConfig()"
|
||||||
@@ -51,16 +50,14 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="pkgStatus && (interfaces | hasUi)"
|
*ngIf="addressInfo | hasUi"
|
||||||
class="action-button"
|
class="action-button"
|
||||||
color="primary"
|
color="primary"
|
||||||
[disabled]="
|
[disabled]="status.primary === 'running'"
|
||||||
!(pkg.state | isLaunchable: pkgStatus.main.status:interfaces)
|
(click)="launchUi(addressInfo)"
|
||||||
"
|
|
||||||
(click)="launchUi()"
|
|
||||||
>
|
>
|
||||||
<ion-icon slot="start" name="open-outline"></ion-icon>
|
<ion-icon slot="start" name="open-outline"></ion-icon>
|
||||||
Launch UI
|
Open UI
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import {
|
|||||||
PrimaryStatus,
|
PrimaryStatus,
|
||||||
} from 'src/app/services/pkg-status-rendering.service'
|
} from 'src/app/services/pkg-status-rendering.service'
|
||||||
import {
|
import {
|
||||||
|
AddressInfo,
|
||||||
DataModel,
|
DataModel,
|
||||||
InterfaceDef,
|
InstalledPackageInfo,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageState,
|
PackageState,
|
||||||
Status,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { ErrorToastService } from '@start9labs/shared'
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
import { AlertController, LoadingController } from '@ionic/angular'
|
import { AlertController, LoadingController } from '@ionic/angular'
|
||||||
@@ -56,12 +56,12 @@ export class AppShowStatusComponent {
|
|||||||
return this.pkg.manifest.id
|
return this.pkg.manifest.id
|
||||||
}
|
}
|
||||||
|
|
||||||
get interfaces(): Record<string, InterfaceDef> {
|
get addressInfo(): Record<string, AddressInfo> {
|
||||||
return this.pkg.manifest.interfaces || {}
|
return this.pkg.installed!['address-info']
|
||||||
}
|
}
|
||||||
|
|
||||||
get pkgStatus(): Status | null {
|
get isConfigured(): boolean {
|
||||||
return this.pkg.installed?.status || null
|
return this.pkg.installed!.status.configured
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInstalled(): boolean {
|
get isInstalled(): boolean {
|
||||||
@@ -76,8 +76,8 @@ export class AppShowStatusComponent {
|
|||||||
return this.status.primary === PrimaryStatus.Stopped
|
return this.status.primary === PrimaryStatus.Stopped
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUi(): void {
|
launchUi(addressInfo: InstalledPackageInfo['address-info']): void {
|
||||||
this.launcherService.launch(this.pkg)
|
this.launcherService.launch(addressInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
async presentModalConfig(): Promise<void> {
|
async presentModalConfig(): Promise<void> {
|
||||||
|
|||||||
@@ -98,15 +98,19 @@ export class ToButtonsPipe implements PipeTransform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||||
|
const { id, version } = pkg.manifest
|
||||||
|
|
||||||
this.apiService
|
this.apiService
|
||||||
.setDbValue<boolean>(['ack-instructions', pkg.manifest.id], true)
|
.setDbValue<boolean>(['ack-instructions', id], true)
|
||||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||||
|
|
||||||
const modal = await this.modalCtrl.create({
|
const modal = await this.modalCtrl.create({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
title: 'Instructions',
|
title: 'Instructions',
|
||||||
content: from(
|
content: from(
|
||||||
this.apiService.getStatic(pkg['static-files']['instructions']),
|
this.apiService.getStatic(
|
||||||
|
`/public/package-data/${id}/${version}/INSTRUCTIONS.md`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
component: MarkdownComponent,
|
component: MarkdownComponent,
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { NavigationExtras } from '@angular/router'
|
|||||||
import { NavController } from '@ionic/angular'
|
import { NavController } from '@ionic/angular'
|
||||||
import {
|
import {
|
||||||
DependencyErrorType,
|
DependencyErrorType,
|
||||||
InstalledPackageDataEntry,
|
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||||
import { ModalService } from 'src/app/services/modal.service'
|
import { ModalService } from 'src/app/services/modal.service'
|
||||||
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
|
|
||||||
export interface DependencyInfo {
|
export interface DependencyInfo {
|
||||||
id: string
|
id: string
|
||||||
@@ -32,40 +32,32 @@ export class ToDependenciesPipe implements PipeTransform {
|
|||||||
if (!pkg.installed) return []
|
if (!pkg.installed) return []
|
||||||
|
|
||||||
return Object.keys(pkg.installed['current-dependencies'])
|
return Object.keys(pkg.installed['current-dependencies'])
|
||||||
.filter(id => !!pkg.manifest.dependencies[id])
|
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||||
.map(id => this.setDepValues(pkg.installed!, id))
|
.map(depId => this.setDepValues(pkg, depId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private setDepValues(
|
private setDepValues(pkg: PackageDataEntry, depId: string): DependencyInfo {
|
||||||
pkg: InstalledPackageDataEntry,
|
|
||||||
id: string,
|
|
||||||
): DependencyInfo {
|
|
||||||
let errorText = ''
|
let errorText = ''
|
||||||
let actionText = 'View'
|
let actionText = 'View'
|
||||||
let action: () => any = () =>
|
let action: () => any = () =>
|
||||||
this.navCtrl.navigateForward(`/services/${id}`)
|
this.navCtrl.navigateForward(`/services/${depId}`)
|
||||||
|
|
||||||
const error = pkg.status['dependency-errors'][id]
|
const error = pkg.installed!.status['dependency-errors'][depId]
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
// health checks failed
|
// health checks failed
|
||||||
if (
|
if (error.type === DependencyErrorType.HealthChecksFailed) {
|
||||||
[
|
|
||||||
DependencyErrorType.InterfaceHealthChecksFailed,
|
|
||||||
DependencyErrorType.HealthChecksFailed,
|
|
||||||
].includes(error.type)
|
|
||||||
) {
|
|
||||||
errorText = 'Health check failed'
|
errorText = 'Health check failed'
|
||||||
// not installed
|
// not installed
|
||||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||||
errorText = 'Not installed'
|
errorText = 'Not installed'
|
||||||
actionText = 'Install'
|
actionText = 'Install'
|
||||||
action = () => this.fixDep(pkg, 'install', id)
|
action = () => this.fixDep(pkg, 'install', depId)
|
||||||
// incorrect version
|
// incorrect version
|
||||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||||
errorText = 'Incorrect version'
|
errorText = 'Incorrect version'
|
||||||
actionText = 'Update'
|
actionText = 'Update'
|
||||||
action = () => this.fixDep(pkg, 'update', id)
|
action = () => this.fixDep(pkg, 'update', depId)
|
||||||
// not running
|
// not running
|
||||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||||
errorText = 'Not running'
|
errorText = 'Not running'
|
||||||
@@ -74,19 +66,19 @@ export class ToDependenciesPipe implements PipeTransform {
|
|||||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||||
errorText = 'Config not satisfied'
|
errorText = 'Config not satisfied'
|
||||||
actionText = 'Auto config'
|
actionText = 'Auto config'
|
||||||
action = () => this.fixDep(pkg, 'configure', id)
|
action = () => this.fixDep(pkg, 'configure', depId)
|
||||||
} else if (error.type === DependencyErrorType.Transitive) {
|
} else if (error.type === DependencyErrorType.Transitive) {
|
||||||
errorText = 'Dependency has a dependency issue'
|
errorText = 'Dependency has a dependency issue'
|
||||||
}
|
}
|
||||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const depInfo = pkg['dependency-info'][id]
|
const depInfo = pkg.installed!['dependency-info'][depId]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: depId,
|
||||||
version: pkg.manifest.dependencies[id].version,
|
version: pkg.manifest.dependencies[depId].version,
|
||||||
title: depInfo?.manifest?.title || id,
|
title: depInfo?.title || depId,
|
||||||
icon: depInfo?.icon || '',
|
icon: depInfo?.icon || '',
|
||||||
errorText,
|
errorText,
|
||||||
actionText,
|
actionText,
|
||||||
@@ -95,28 +87,25 @@ export class ToDependenciesPipe implements PipeTransform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fixDep(
|
async fixDep(
|
||||||
pkg: InstalledPackageDataEntry,
|
pkg: PackageDataEntry,
|
||||||
action: 'install' | 'update' | 'configure',
|
action: 'install' | 'update' | 'configure',
|
||||||
id: string,
|
depId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'install':
|
case 'install':
|
||||||
case 'update':
|
case 'update':
|
||||||
return this.installDep(pkg, id)
|
return this.installDep(pkg.manifest, depId)
|
||||||
case 'configure':
|
case 'configure':
|
||||||
return this.configureDep(pkg, id)
|
return this.configureDep(pkg.manifest, depId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async installDep(
|
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||||
pkg: InstalledPackageDataEntry,
|
const version = manifest.dependencies[depId].version
|
||||||
depId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const version = pkg.manifest.dependencies[depId].version
|
|
||||||
|
|
||||||
const dependentInfo: DependentInfo = {
|
const dependentInfo: DependentInfo = {
|
||||||
id: pkg.manifest.id,
|
id: manifest.id,
|
||||||
title: pkg.manifest.title,
|
title: manifest.title,
|
||||||
version,
|
version,
|
||||||
}
|
}
|
||||||
const navigationExtras: NavigationExtras = {
|
const navigationExtras: NavigationExtras = {
|
||||||
@@ -130,12 +119,12 @@ export class ToDependenciesPipe implements PipeTransform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async configureDep(
|
private async configureDep(
|
||||||
pkg: InstalledPackageDataEntry,
|
manifest: Manifest,
|
||||||
dependencyId: string,
|
dependencyId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const dependentInfo: DependentInfo = {
|
const dependentInfo: DependentInfo = {
|
||||||
id: pkg.manifest.id,
|
id: manifest.id,
|
||||||
title: pkg.manifest.title,
|
title: manifest.title,
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.modalService.presentModalConfig({
|
await this.modalService.presentModalConfig({
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
|
|||||||
},
|
},
|
||||||
'marketing-site': {
|
'marketing-site': {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'Marketing Site',
|
name: 'Website',
|
||||||
description: 'URL to the marketing site / channel for the project',
|
description: 'URL to the marketing site / channel for the project',
|
||||||
placeholder: 'e.g. start9.com',
|
placeholder: 'e.g. start9.com',
|
||||||
pattern: null,
|
pattern: null,
|
||||||
|
|||||||
@@ -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; else install">
|
||||||
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
<ng-container *ngIf="localPkg.state === PackageState.Installed">
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1"
|
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === -1"
|
||||||
expand="block"
|
expand="block"
|
||||||
color="success"
|
color="success"
|
||||||
(click)="tryInstall()"
|
(click)="tryInstall()"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
Update
|
Update
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 1"
|
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 1"
|
||||||
expand="block"
|
expand="block"
|
||||||
color="warning"
|
color="warning"
|
||||||
(click)="tryInstall()"
|
(click)="tryInstall()"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</ion-button>
|
</ion-button>
|
||||||
<ng-container *ngIf="showDevTools$ | async">
|
<ng-container *ngIf="showDevTools$ | async">
|
||||||
<ion-button
|
<ion-button
|
||||||
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 0"
|
*ngIf="(localVersion | compareEmver : pkg.manifest.version) === 0"
|
||||||
expand="block"
|
expand="block"
|
||||||
color="success"
|
color="success"
|
||||||
(click)="tryInstall()"
|
(click)="tryInstall()"
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<span
|
<span
|
||||||
*ngIf="version | satisfiesEmver: dependentInfo.version"
|
*ngIf="version | satisfiesEmver : dependentInfo.version"
|
||||||
class="text"
|
class="text"
|
||||||
>
|
>
|
||||||
{{ title }} version {{ version | displayEmver }} is compatible.
|
{{ title }} version {{ version | displayEmver }} is compatible.
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
*ngIf="!(version | satisfiesEmver: dependentInfo.version)"
|
*ngIf="!(version | satisfiesEmver : dependentInfo.version)"
|
||||||
class="text text_error"
|
class="text text_error"
|
||||||
>
|
>
|
||||||
{{ title }} version {{ version | displayEmver }} is NOT compatible.
|
{{ title }} version {{ version | displayEmver }} is NOT compatible.
|
||||||
@@ -16,9 +16,7 @@ import {
|
|||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
|
||||||
import { MarketplaceShowPage } from './marketplace-show.page'
|
import { MarketplaceShowPage } from './marketplace-show.page'
|
||||||
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
|
import { MarketplaceShowComponentsModule } from './components/marketplace-show-components.module'
|
||||||
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
|
|
||||||
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -41,18 +39,8 @@ const routes: Routes = [
|
|||||||
AboutModule,
|
AboutModule,
|
||||||
DependenciesModule,
|
DependenciesModule,
|
||||||
AdditionalModule,
|
AdditionalModule,
|
||||||
|
MarketplaceShowComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [MarketplaceShowPage],
|
||||||
MarketplaceShowPage,
|
|
||||||
MarketplaceShowHeaderComponent,
|
|
||||||
MarketplaceShowControlsComponent,
|
|
||||||
MarketplaceShowDependentComponent,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
MarketplaceShowPage,
|
|
||||||
MarketplaceShowHeaderComponent,
|
|
||||||
MarketplaceShowControlsComponent,
|
|
||||||
MarketplaceShowDependentComponent,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class MarketplaceShowPageModule {}
|
export class MarketplaceShowPageModule {}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<ng-container *ngFor="let pkg of pkgs | keyvalue">
|
<ng-container *ngFor="let pkg of pkgs | keyvalue">
|
||||||
<ion-item *ngIf="backupProgress[pkg.key] as pkgProgress">
|
<ion-item *ngIf="backupProgress[pkg.key] as pkgProgress">
|
||||||
<ion-avatar slot="start">
|
<ion-avatar slot="start">
|
||||||
<img [src]="pkg.value['static-files'].icon" />
|
<img [src]="pkg.value.icon" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label> {{ pkg.value.manifest.title }} </ion-label>
|
<ion-label>{{ pkg.value.manifest.title }}</ion-label>
|
||||||
<!-- complete -->
|
<!-- complete -->
|
||||||
<ion-note
|
<ion-note
|
||||||
*ngIf="pkgProgress.complete; else incomplete"
|
*ngIf="pkgProgress.complete; else incomplete"
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import { SideloadPage } from './sideload.page'
|
|||||||
import { Routes, RouterModule } from '@angular/router'
|
import { Routes, RouterModule } from '@angular/router'
|
||||||
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
|
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
|
||||||
import { DragNDropDirective } from './dnd.directive'
|
import { DragNDropDirective } from './dnd.directive'
|
||||||
|
import {
|
||||||
|
PackageModule,
|
||||||
|
AboutModule,
|
||||||
|
AdditionalModule,
|
||||||
|
DependenciesModule,
|
||||||
|
} from '@start9labs/marketplace'
|
||||||
|
import { MarketplaceShowComponentsModule } from '../../marketplace-routes/marketplace-show/components/marketplace-show-components.module'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -20,6 +27,11 @@ const routes: Routes = [
|
|||||||
RouterModule.forChild(routes),
|
RouterModule.forChild(routes),
|
||||||
SharedPipesModule,
|
SharedPipesModule,
|
||||||
EmverPipesModule,
|
EmverPipesModule,
|
||||||
|
PackageModule,
|
||||||
|
AboutModule,
|
||||||
|
AdditionalModule,
|
||||||
|
DependenciesModule,
|
||||||
|
MarketplaceShowComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [SideloadPage, DragNDropDirective],
|
declarations: [SideloadPage, DragNDropDirective],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,92 +7,70 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-text-center with-widgets">
|
<ion-content class="ion-padding with-widgets">
|
||||||
<!-- file upload -->
|
<!-- invalid -->
|
||||||
<div
|
<div *ngIf="invalid; else valid" class="drop-area_filled">
|
||||||
*ngIf="!toUpload.file; else fileUploaded"
|
<h4>
|
||||||
class="drop-area"
|
<ion-icon
|
||||||
[class.drop-area_mobile]="isMobile"
|
name="close-circle-outline"
|
||||||
appDnd
|
color="danger"
|
||||||
(onFileDropped)="handleFileDrop($event)"
|
class="inline"
|
||||||
>
|
></ion-icon>
|
||||||
<ion-icon
|
Invalid package file
|
||||||
name="cloud-upload-outline"
|
</h4>
|
||||||
color="dark"
|
<ion-button color="primary" (click)="clear()">Try again</ion-button>
|
||||||
style="font-size: 42px"
|
|
||||||
></ion-icon>
|
|
||||||
<h4>Upload .s9pk package file</h4>
|
|
||||||
<p *ngIf="onTor">
|
|
||||||
<ion-text color="success"
|
|
||||||
>Tip: switch to LAN for faster uploads.</ion-text
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<ion-button color="primary" type="file" class="ion-margin-top">
|
|
||||||
<label for="upload-photo">Browse</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
style="position: absolute; opacity: 0; height: 100%"
|
|
||||||
id="upload-photo"
|
|
||||||
(change)="handleFileInput($event)"
|
|
||||||
/>
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- file uploaded -->
|
|
||||||
<ng-template #fileUploaded>
|
<!-- valid -->
|
||||||
<div class="drop-area_filled">
|
<ng-template #valid>
|
||||||
<h4>
|
<!-- uploaded -->
|
||||||
<ion-icon
|
<div class="ion-padding" *ngIf="pkgData?.pkg as pkg; else empty">
|
||||||
*ngIf="uploadState?.invalid"
|
<div class="ion-text-right">
|
||||||
name="close-circle-outline"
|
<ion-button color="danger" (click)="clear()">
|
||||||
color="danger"
|
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||||
class="inline"
|
|
||||||
></ion-icon>
|
|
||||||
<ion-icon
|
|
||||||
*ngIf="!uploadState?.invalid"
|
|
||||||
class="inline"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
color="success"
|
|
||||||
></ion-icon>
|
|
||||||
{{ uploadState?.message }}
|
|
||||||
</h4>
|
|
||||||
<div class="box" *ngIf="toUpload.icon && toUpload.manifest">
|
|
||||||
<div class="card">
|
|
||||||
<div class="row row_end">
|
|
||||||
<ion-button
|
|
||||||
style="
|
|
||||||
--background-hover: transparent;
|
|
||||||
--padding-end: 0px;
|
|
||||||
--padding-start: 0px;
|
|
||||||
"
|
|
||||||
fill="clear"
|
|
||||||
size="small"
|
|
||||||
(click)="clearToUpload()"
|
|
||||||
>
|
|
||||||
<ion-icon slot="icon-only" name="close" color="danger"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<img
|
|
||||||
[alt]="toUpload.manifest.title + ' Icon'"
|
|
||||||
[src]="toUpload.icon | trustUrl"
|
|
||||||
/>
|
|
||||||
<h2>{{ toUpload.manifest.title }}</h2>
|
|
||||||
<p>{{ toUpload.manifest.version | displayEmver }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="!toUpload.icon && !toUpload.manifest; else uploadButton"
|
|
||||||
color="primary"
|
|
||||||
(click)="clearToUpload()"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</ion-button>
|
|
||||||
<ng-template #uploadButton>
|
|
||||||
<ion-button color="primary" (click)="handleUpload()">
|
|
||||||
Upload & Install
|
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ng-template>
|
</div>
|
||||||
|
<marketplace-package [pkg]="pkg"></marketplace-package>
|
||||||
|
<marketplace-show-controls [pkg]="pkg"></marketplace-show-controls>
|
||||||
|
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
|
||||||
|
<ion-item-group>
|
||||||
|
<marketplace-about [pkg]="pkg"></marketplace-about>
|
||||||
|
<marketplace-dependencies
|
||||||
|
*ngIf="!(pkg.manifest.dependencies | empty)"
|
||||||
|
[pkg]="pkg"
|
||||||
|
></marketplace-dependencies>
|
||||||
|
</ion-item-group>
|
||||||
|
<marketplace-additional [pkg]="pkg"></marketplace-additional>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- empty -->
|
||||||
|
<ng-template #empty>
|
||||||
|
<div
|
||||||
|
class="drop-area"
|
||||||
|
[class.drop-area_mobile]="isMobile"
|
||||||
|
appDnd
|
||||||
|
(onFileDropped)="handleFileDrop($event)"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
name="cloud-upload-outline"
|
||||||
|
color="dark"
|
||||||
|
style="font-size: 42px"
|
||||||
|
></ion-icon>
|
||||||
|
<h4>Upload .s9pk package file</h4>
|
||||||
|
<p *ngIf="onTor">
|
||||||
|
<ion-text color="success">
|
||||||
|
Tip: switch to LAN for faster uploads.
|
||||||
|
</ion-text>
|
||||||
|
</p>
|
||||||
|
<ion-button color="primary" type="file" class="ion-margin-top">
|
||||||
|
<label for="upload-photo">Browse</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
style="position: absolute; opacity: 0; height: 100%"
|
||||||
|
id="upload-photo"
|
||||||
|
(change)="handleFileInput($event)"
|
||||||
|
/>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
vertical-align: initial;
|
vertical-align: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.area {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-area {
|
.drop-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: rgba(24, 24, 24, 0.5);
|
background-color: rgba(24, 24, 24, 0.5);
|
||||||
@@ -18,7 +13,7 @@
|
|||||||
border-color: var(--ion-color-dark);
|
border-color: var(--ion-color-dark);
|
||||||
color: var(--ion-color-dark);
|
color: var(--ion-color-dark);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 60px;
|
margin: 20px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
|
|
||||||
@@ -47,45 +42,3 @@
|
|||||||
color: var(--ion-color-dark);
|
color: var(--ion-color-dark);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: radial-gradient(var(--ion-color-step-100), transparent);
|
|
||||||
min-width: 200px;
|
|
||||||
max-width: 200px;
|
|
||||||
height: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px 20px 40px 20px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--ion-color-step-100);
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 4px 8px 8px 8px;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
&_end {
|
|
||||||
align-self: end;
|
|
||||||
|
|
||||||
ion-button {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
p {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { isPlatform, LoadingController, NavController } from '@ionic/angular'
|
import { isPlatform, LoadingController, NavController } from '@ionic/angular'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { Manifest } from 'src/app/services/patch-db/data-model'
|
import { Manifest, MarketplacePkg } from '@start9labs/marketplace'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import cbor from 'cbor'
|
|
||||||
import { ErrorToastService } from '@start9labs/shared'
|
import { ErrorToastService } from '@start9labs/shared'
|
||||||
|
import cbor from 'cbor'
|
||||||
|
|
||||||
interface Positions {
|
interface Positions {
|
||||||
[key: string]: [bigint, bigint] // [position, length]
|
[key: string]: [bigint, bigint] // [position, length]
|
||||||
@@ -20,20 +20,12 @@ const VERSION = new Uint8Array([1])
|
|||||||
})
|
})
|
||||||
export class SideloadPage {
|
export class SideloadPage {
|
||||||
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
|
isMobile = isPlatform(window, 'ios') || isPlatform(window, 'android')
|
||||||
toUpload: {
|
pkgData?: {
|
||||||
manifest: Manifest | null
|
pkg: MarketplacePkg
|
||||||
icon: string | null
|
file: File
|
||||||
file: File | null
|
|
||||||
} = {
|
|
||||||
manifest: null,
|
|
||||||
icon: null,
|
|
||||||
file: null,
|
|
||||||
}
|
}
|
||||||
onTor = this.config.isTor()
|
onTor = this.config.isTor()
|
||||||
uploadState?: {
|
invalid = false
|
||||||
invalid: boolean
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly loadingCtrl: LoadingController,
|
private readonly loadingCtrl: LoadingController,
|
||||||
@@ -53,63 +45,60 @@ export class SideloadPage {
|
|||||||
this.setFile(files)
|
this.setFile(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
async setFile(files?: File[]) {
|
clear() {
|
||||||
if (!files || !files.length) return
|
this.pkgData = undefined
|
||||||
const file = files[0]
|
this.invalid = false
|
||||||
if (!file) return
|
|
||||||
this.toUpload.file = file
|
|
||||||
this.uploadState = await this.validateS9pk(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateS9pk(file: File) {
|
|
||||||
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
|
|
||||||
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
|
|
||||||
if (compare(magic, MAGIC) && compare(version, VERSION)) {
|
|
||||||
await this.parseS9pk(file)
|
|
||||||
return {
|
|
||||||
invalid: false,
|
|
||||||
message: 'A valid package file has been detected!',
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
invalid: true,
|
|
||||||
message: 'Invalid package file',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearToUpload() {
|
|
||||||
this.toUpload.file = null
|
|
||||||
this.toUpload.manifest = null
|
|
||||||
this.toUpload.icon = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUpload() {
|
async handleUpload() {
|
||||||
|
if (!this.pkgData) return
|
||||||
const loader = await this.loadingCtrl.create({
|
const loader = await this.loadingCtrl.create({
|
||||||
message: 'Uploading package',
|
message: 'Uploading package',
|
||||||
cssClass: 'loader',
|
cssClass: 'loader',
|
||||||
})
|
})
|
||||||
await loader.present()
|
await loader.present()
|
||||||
|
|
||||||
|
const { pkg, file } = this.pkgData
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const guid = await this.api.sideloadPackage({
|
const guid = await this.api.sideloadPackage({
|
||||||
manifest: this.toUpload.manifest!,
|
manifest: pkg.manifest,
|
||||||
icon: this.toUpload.icon!,
|
icon: pkg.icon,
|
||||||
size: this.toUpload.file!.size,
|
size: file.size,
|
||||||
})
|
})
|
||||||
this.api
|
this.api.uploadPackage(guid, file).catch(e => console.error(e))
|
||||||
.uploadPackage(guid, this.toUpload.file!)
|
|
||||||
.catch(e => console.error(e))
|
|
||||||
|
|
||||||
this.navCtrl.navigateRoot('/services')
|
this.navCtrl.navigateRoot('/services')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errToast.present(e)
|
this.errToast.present(e)
|
||||||
} finally {
|
} finally {
|
||||||
loader.dismiss()
|
loader.dismiss()
|
||||||
this.clearToUpload()
|
this.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseS9pk(file: File) {
|
private async setFile(files?: File[]) {
|
||||||
|
if (!files || !files.length) return
|
||||||
|
const file = files[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
await this.validateS9pk(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateS9pk(file: File) {
|
||||||
|
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
|
||||||
|
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
|
||||||
|
if (compare(magic, MAGIC) && compare(version, VERSION)) {
|
||||||
|
this.pkgData = {
|
||||||
|
pkg: await this.parseS9pk(file),
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.invalid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseS9pk(file: File): Promise<MarketplacePkg> {
|
||||||
const positions: Positions = {}
|
const positions: Positions = {}
|
||||||
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
|
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
|
||||||
let start = 103
|
let start = 103
|
||||||
@@ -119,30 +108,51 @@ export class SideloadPage {
|
|||||||
).getUint32(0, false)
|
).getUint32(0, false)
|
||||||
await getPositions(start, end, file, positions, tocLength as any)
|
await getPositions(start, end, file, positions, tocLength as any)
|
||||||
|
|
||||||
await this.getManifest(positions, file)
|
const manifest = await this.getAsset(positions, file, 'manifest')
|
||||||
await this.getIcon(positions, file)
|
const [icon] = await Promise.all([
|
||||||
|
this.getIcon(positions, file, manifest),
|
||||||
|
// this.getAsset(positions, file, 'license'),
|
||||||
|
// this.getAsset(positions, file, 'instructions'),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
manifest,
|
||||||
|
icon,
|
||||||
|
license: '',
|
||||||
|
instructions: '',
|
||||||
|
categories: [],
|
||||||
|
versions: [],
|
||||||
|
'dependency-metadata': {},
|
||||||
|
'published-at': '',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getManifest(positions: Positions, file: Blob) {
|
private async getAsset(
|
||||||
|
positions: Positions,
|
||||||
|
file: Blob,
|
||||||
|
asset: 'manifest' | 'license' | 'instructions',
|
||||||
|
): Promise<any> {
|
||||||
const data = await blobToBuffer(
|
const data = await blobToBuffer(
|
||||||
file.slice(
|
file.slice(
|
||||||
Number(positions['manifest'][0]),
|
Number(positions[asset][0]),
|
||||||
Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
|
Number(positions[asset][0]) + Number(positions[asset][1]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
this.toUpload.manifest = await cbor.decode(data, true)
|
return cbor.decode(data, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIcon(positions: Positions, file: Blob) {
|
private async getIcon(
|
||||||
const contentType = `image/${this.toUpload.manifest?.assets.icon
|
positions: Positions,
|
||||||
.split('.')
|
file: Blob,
|
||||||
.pop()}`
|
manifest: Manifest,
|
||||||
|
): Promise<string> {
|
||||||
|
const contentType = `image/${manifest.assets.icon.split('.').pop()}`
|
||||||
const data = file.slice(
|
const data = file.slice(
|
||||||
Number(positions['icon'][0]),
|
Number(positions['icon'][0]),
|
||||||
Number(positions['icon'][0]) + Number(positions['icon'][1]),
|
Number(positions['icon'][0]) + Number(positions['icon'][1]),
|
||||||
contentType,
|
contentType,
|
||||||
)
|
)
|
||||||
this.toUpload.icon = await blobToDataURL(data)
|
return blobToDataURL(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
|||||||
import {
|
import {
|
||||||
AbstractMarketplaceService,
|
AbstractMarketplaceService,
|
||||||
Marketplace,
|
Marketplace,
|
||||||
MarketplaceManifest,
|
Manifest,
|
||||||
MarketplacePkg,
|
MarketplacePkg,
|
||||||
StoreIdentity,
|
StoreIdentity,
|
||||||
} from '@start9labs/marketplace'
|
} from '@start9labs/marketplace'
|
||||||
@@ -64,7 +64,7 @@ export class UpdatesPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tryUpdate(
|
async tryUpdate(
|
||||||
manifest: MarketplaceManifest,
|
manifest: Manifest,
|
||||||
url: string,
|
url: string,
|
||||||
local: PackageDataEntry,
|
local: PackageDataEntry,
|
||||||
e: Event,
|
e: Event,
|
||||||
@@ -83,7 +83,7 @@ export class UpdatesPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async dryUpdate(manifest: MarketplaceManifest, url: string) {
|
private async dryUpdate(manifest: Manifest, url: string) {
|
||||||
const loader = await this.loadingCtrl.create({
|
const loader = await this.loadingCtrl.create({
|
||||||
message: 'Checking dependent services...',
|
message: 'Checking dependent services...',
|
||||||
})
|
})
|
||||||
@@ -181,7 +181,7 @@ export class FilterUpdatesPipe implements PipeTransform {
|
|||||||
({ manifest }) =>
|
({ manifest }) =>
|
||||||
this.emver.compare(
|
this.emver.compare(
|
||||||
manifest.version,
|
manifest.version,
|
||||||
local[manifest.id]?.installed?.manifest.version || '',
|
local[manifest.id]?.manifest.version || '', // @TODO this won't work, need old version
|
||||||
) === 1,
|
) === 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { InterfaceDef } from '../../services/patch-db/data-model'
|
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import { hasUi } from '../../services/config.service'
|
import { hasUi } from '../../services/config.service'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'hasUi',
|
name: 'hasUi',
|
||||||
})
|
})
|
||||||
export class UiPipe implements PipeTransform {
|
export class UiPipe implements PipeTransform {
|
||||||
transform(interfaces: Record<string, InterfaceDef>): boolean {
|
transform(addressInfo: InstalledPackageInfo['address-info']): boolean {
|
||||||
return hasUi(interfaces)
|
return hasUi(addressInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
DependencyErrorType,
|
DependencyErrorType,
|
||||||
DockerIoFormat,
|
|
||||||
Manifest,
|
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
@@ -9,7 +7,11 @@ import {
|
|||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types'
|
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types'
|
||||||
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
|
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
|
||||||
import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace'
|
import {
|
||||||
|
DependencyMetadata,
|
||||||
|
MarketplacePkg,
|
||||||
|
Manifest,
|
||||||
|
} from '@start9labs/marketplace'
|
||||||
import { Log } from '@start9labs/shared'
|
import { Log } from '@start9labs/shared'
|
||||||
|
|
||||||
export module Mock {
|
export module Mock {
|
||||||
@@ -17,6 +19,7 @@ export module Mock {
|
|||||||
'backup-progress': null,
|
'backup-progress': null,
|
||||||
'update-progress': null,
|
'update-progress': null,
|
||||||
updated: true,
|
updated: true,
|
||||||
|
'shutting-down': false,
|
||||||
}
|
}
|
||||||
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
|
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
|
||||||
version: '0.3.4.3',
|
version: '0.3.4.3',
|
||||||
@@ -50,16 +53,11 @@ export module Mock {
|
|||||||
short: 'A Bitcoin full node by Bitcoin Core.',
|
short: 'A Bitcoin full node by Bitcoin Core.',
|
||||||
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
|
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
|
||||||
},
|
},
|
||||||
replaces: ['banks', 'governments'],
|
|
||||||
'release-notes': 'Taproot, Schnorr, and more.',
|
|
||||||
assets: {
|
assets: {
|
||||||
icon: 'icon.png',
|
icon: 'icon.png',
|
||||||
license: 'LICENSE.md',
|
|
||||||
instructions: 'INSTRUCTIONS.md',
|
|
||||||
docker_images: 'image.tar',
|
|
||||||
assets: './assets',
|
|
||||||
scripts: './scripts',
|
|
||||||
},
|
},
|
||||||
|
replaces: ['banks', 'governments'],
|
||||||
|
'release-notes': 'Taproot, Schnorr, and more.',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
|
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
|
||||||
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
|
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
|
||||||
@@ -74,299 +72,8 @@ export module Mock {
|
|||||||
start: 'Starting Bitcoin is good for your health.',
|
start: 'Starting Bitcoin is good for your health.',
|
||||||
stop: null,
|
stop: null,
|
||||||
},
|
},
|
||||||
main: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': '1ms',
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
get: null,
|
|
||||||
set: null,
|
|
||||||
},
|
|
||||||
volumes: {},
|
|
||||||
'min-os-version': '0.2.12',
|
|
||||||
interfaces: {
|
|
||||||
ui: {
|
|
||||||
name: 'Node Visualizer',
|
|
||||||
description:
|
|
||||||
'Web application for viewing information about your node and the Bitcoin network.',
|
|
||||||
ui: true,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
rpc: {
|
|
||||||
name: 'RPC',
|
|
||||||
description: 'Used by wallets to interact with your Bitcoin Core node.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
p2p: {
|
|
||||||
name: 'P2P',
|
|
||||||
description:
|
|
||||||
'Used by other Bitcoin nodes to communicate and interact with your node.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backup: {
|
|
||||||
create: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
restore: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
migrations: null,
|
|
||||||
actions: {
|
|
||||||
resync: {
|
|
||||||
name: 'Resync Blockchain',
|
|
||||||
description: 'Use this to resync the Bitcoin blockchain from genesis',
|
|
||||||
warning: 'This will take a couple of days.',
|
|
||||||
'allowed-statuses': [
|
|
||||||
PackageMainStatus.Running,
|
|
||||||
PackageMainStatus.Stopped,
|
|
||||||
],
|
|
||||||
implementation: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
'input-spec': {
|
|
||||||
reason: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Re-sync Reason',
|
|
||||||
description: 'Your reason for re-syncing. Why are you doing this?',
|
|
||||||
placeholder: null,
|
|
||||||
nullable: false,
|
|
||||||
masked: false,
|
|
||||||
pattern: '^[a-zA-Z]+$',
|
|
||||||
'pattern-description': 'Must contain only letters.',
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Your Name',
|
|
||||||
description: 'Tell the class your name.',
|
|
||||||
nullable: true,
|
|
||||||
masked: false,
|
|
||||||
warning: 'You may loose all your money by providing your name.',
|
|
||||||
placeholder: null,
|
|
||||||
pattern: null,
|
|
||||||
'pattern-description': null,
|
|
||||||
textarea: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
notifications: {
|
|
||||||
name: 'Notification Preferences',
|
|
||||||
type: 'list',
|
|
||||||
subtype: 'enum',
|
|
||||||
description: 'how you want to be notified',
|
|
||||||
warning: null,
|
|
||||||
range: '[1,3]',
|
|
||||||
default: ['email'],
|
|
||||||
spec: {
|
|
||||||
'value-names': {
|
|
||||||
email: 'Email',
|
|
||||||
text: 'Text',
|
|
||||||
call: 'Call',
|
|
||||||
push: 'Push',
|
|
||||||
webhook: 'Webhook',
|
|
||||||
},
|
|
||||||
values: ['email', 'text', 'call', 'push', 'webhook'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'days-ago': {
|
|
||||||
type: 'number',
|
|
||||||
name: 'Days Ago',
|
|
||||||
description: 'Number of days to re-sync.',
|
|
||||||
nullable: false,
|
|
||||||
default: 100,
|
|
||||||
range: '[0, 9999]',
|
|
||||||
integral: true,
|
|
||||||
units: null,
|
|
||||||
placeholder: null,
|
|
||||||
warning: null,
|
|
||||||
},
|
|
||||||
'top-speed': {
|
|
||||||
type: 'number',
|
|
||||||
name: 'Top Speed',
|
|
||||||
description: 'The fastest you can possibly run.',
|
|
||||||
nullable: false,
|
|
||||||
range: '[-1000, 1000]',
|
|
||||||
integral: false,
|
|
||||||
units: 'm/s',
|
|
||||||
placeholder: null,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
testnet: {
|
|
||||||
name: 'Testnet',
|
|
||||||
type: 'boolean',
|
|
||||||
description:
|
|
||||||
'<ul><li>determines whether your node is running on testnet or mainnet</li></ul><script src="fake"></script>',
|
|
||||||
warning: 'Chain will have to resync!',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
randomEnum: {
|
|
||||||
name: 'Random Enum',
|
|
||||||
type: 'enum',
|
|
||||||
'value-names': {
|
|
||||||
null: 'Null',
|
|
||||||
good: 'Good',
|
|
||||||
bad: 'Bad',
|
|
||||||
ugly: 'Ugly',
|
|
||||||
},
|
|
||||||
default: 'null',
|
|
||||||
description: 'This is not even real.',
|
|
||||||
warning: 'Be careful changing this!',
|
|
||||||
values: ['null', 'good', 'bad', 'ugly'],
|
|
||||||
},
|
|
||||||
'emergency-contact': {
|
|
||||||
name: 'Emergency Contact',
|
|
||||||
type: 'object',
|
|
||||||
description: 'The person to contact in case of emergency.',
|
|
||||||
warning: null,
|
|
||||||
spec: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Name',
|
|
||||||
description: null,
|
|
||||||
nullable: false,
|
|
||||||
masked: false,
|
|
||||||
pattern: '^[a-zA-Z]+$',
|
|
||||||
'pattern-description': 'Must contain only letters.',
|
|
||||||
placeholder: null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Email',
|
|
||||||
description: null,
|
|
||||||
nullable: false,
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
pattern: null,
|
|
||||||
'pattern-description': null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ips: {
|
|
||||||
name: 'Whitelist IPs',
|
|
||||||
type: 'list',
|
|
||||||
subtype: 'string',
|
|
||||||
description:
|
|
||||||
'external ip addresses that are authorized to access your Bitcoin node',
|
|
||||||
warning:
|
|
||||||
'Any IP you allow here will have RPC access to your Bitcoin node.',
|
|
||||||
range: '[1,10]',
|
|
||||||
default: ['192.168.1.1'],
|
|
||||||
spec: {
|
|
||||||
placeholder: null,
|
|
||||||
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
|
|
||||||
'pattern-description': 'Must be a valid IP address',
|
|
||||||
masked: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bitcoinNode: {
|
|
||||||
type: 'union',
|
|
||||||
default: 'internal',
|
|
||||||
tag: {
|
|
||||||
id: 'type',
|
|
||||||
'variant-names': {
|
|
||||||
internal: 'Internal',
|
|
||||||
external: 'External',
|
|
||||||
},
|
|
||||||
name: 'Bitcoin Node Settings',
|
|
||||||
description: 'The node settings',
|
|
||||||
warning: 'Careful changing this',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
internal: {
|
|
||||||
'friendly-name': {
|
|
||||||
name: 'Friendly Name',
|
|
||||||
type: 'string',
|
|
||||||
description: 'the lan address',
|
|
||||||
nullable: true,
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
pattern: null,
|
|
||||||
'pattern-description': null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
external: {
|
|
||||||
'public-domain': {
|
|
||||||
name: 'Public Domain',
|
|
||||||
type: 'string',
|
|
||||||
description: 'the public address of the node',
|
|
||||||
nullable: false,
|
|
||||||
default: 'bitcoinnode.com',
|
|
||||||
pattern: '.*',
|
|
||||||
'pattern-description': 'anything',
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies: {},
|
dependencies: {},
|
||||||
|
'os-version': '0.4.0',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockManifestLnd: Manifest = {
|
export const MockManifestLnd: Manifest = {
|
||||||
@@ -377,15 +84,10 @@ export module Mock {
|
|||||||
short: 'A bolt spec compliant client.',
|
short: 'A bolt spec compliant client.',
|
||||||
long: 'More info about LND. More info about LND. More info about LND.',
|
long: 'More info about LND. More info about LND. More info about LND.',
|
||||||
},
|
},
|
||||||
'release-notes': 'Dual funded channels!',
|
|
||||||
assets: {
|
assets: {
|
||||||
icon: 'icon.png',
|
icon: 'icon.png',
|
||||||
license: 'LICENSE.md',
|
|
||||||
instructions: 'INSTRUCTIONS.md',
|
|
||||||
docker_images: 'image.tar',
|
|
||||||
assets: './assets',
|
|
||||||
scripts: './scripts',
|
|
||||||
},
|
},
|
||||||
|
'release-notes': 'Dual funded channels!',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
|
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
|
||||||
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
|
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
|
||||||
@@ -400,104 +102,6 @@ export module Mock {
|
|||||||
start: 'Starting LND is good for your health.',
|
start: 'Starting LND is good for your health.',
|
||||||
stop: null,
|
stop: null,
|
||||||
},
|
},
|
||||||
main: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': '10000µs',
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
get: null,
|
|
||||||
set: null,
|
|
||||||
},
|
|
||||||
volumes: {},
|
|
||||||
'min-os-version': '0.2.12',
|
|
||||||
interfaces: {
|
|
||||||
rpc: {
|
|
||||||
name: 'RPC interface',
|
|
||||||
description: 'Good for connecting to your node at a distance.',
|
|
||||||
ui: true,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {
|
|
||||||
'44': {
|
|
||||||
ssl: true,
|
|
||||||
mapping: 33,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
grpc: {
|
|
||||||
name: 'GRPC',
|
|
||||||
description: 'Certain wallet use grpc.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {
|
|
||||||
'66': {
|
|
||||||
ssl: true,
|
|
||||||
mapping: 55,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backup: {
|
|
||||||
create: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
restore: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
migrations: null,
|
|
||||||
actions: {
|
|
||||||
resync: {
|
|
||||||
name: 'Resync Network Graph',
|
|
||||||
description: 'Your node will resync its network graph.',
|
|
||||||
warning: 'This will take a couple hours.',
|
|
||||||
'allowed-statuses': [PackageMainStatus.Running],
|
|
||||||
implementation: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
'input-spec': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
version: '=0.21.0',
|
version: '=0.21.0',
|
||||||
@@ -506,7 +110,6 @@ export module Mock {
|
|||||||
type: 'opt-out',
|
type: 'opt-out',
|
||||||
how: 'You can use an external node from your server if you prefer.',
|
how: 'You can use an external node from your server if you prefer.',
|
||||||
},
|
},
|
||||||
config: null,
|
|
||||||
},
|
},
|
||||||
'btc-rpc-proxy': {
|
'btc-rpc-proxy': {
|
||||||
version: '>=0.2.2',
|
version: '>=0.2.2',
|
||||||
@@ -516,9 +119,9 @@ export module Mock {
|
|||||||
type: 'opt-in',
|
type: 'opt-in',
|
||||||
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
|
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
|
||||||
},
|
},
|
||||||
config: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'os-version': '0.4.0',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockManifestBitcoinProxy: Manifest = {
|
export const MockManifestBitcoinProxy: Manifest = {
|
||||||
@@ -530,15 +133,10 @@ export module Mock {
|
|||||||
short: 'A super charger for your Bitcoin node.',
|
short: 'A super charger for your Bitcoin node.',
|
||||||
long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.',
|
long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.',
|
||||||
},
|
},
|
||||||
'release-notes': 'Even better support for Bitcoin and wallets!',
|
|
||||||
assets: {
|
assets: {
|
||||||
icon: 'icon.png',
|
icon: 'icon.png',
|
||||||
license: 'LICENSE.md',
|
|
||||||
instructions: 'INSTRUCTIONS.md',
|
|
||||||
docker_images: 'image.tar',
|
|
||||||
assets: './assets',
|
|
||||||
scripts: './scripts',
|
|
||||||
},
|
},
|
||||||
|
'release-notes': 'Even better support for Bitcoin and wallets!',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
|
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
|
||||||
'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy',
|
'upstream-repo': 'https://github.com/Kixunil/btc-rpc-proxy',
|
||||||
@@ -552,66 +150,6 @@ export module Mock {
|
|||||||
start: null,
|
start: null,
|
||||||
stop: null,
|
stop: null,
|
||||||
},
|
},
|
||||||
main: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [''],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': '1m',
|
|
||||||
},
|
|
||||||
config: { get: {} as any, set: {} as any },
|
|
||||||
volumes: {},
|
|
||||||
'min-os-version': '0.2.12',
|
|
||||||
interfaces: {
|
|
||||||
rpc: {
|
|
||||||
name: 'RPC interface',
|
|
||||||
description: 'Good for connecting to your node at a distance.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {
|
|
||||||
44: {
|
|
||||||
ssl: true,
|
|
||||||
mapping: 33,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backup: {
|
|
||||||
create: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [''],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
restore: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [''],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
migrations: null,
|
|
||||||
actions: {},
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
version: '>=0.20.0',
|
version: '>=0.20.0',
|
||||||
@@ -619,34 +157,9 @@ export module Mock {
|
|||||||
requirement: {
|
requirement: {
|
||||||
type: 'required',
|
type: 'required',
|
||||||
},
|
},
|
||||||
config: {
|
|
||||||
check: {
|
|
||||||
type: 'docker',
|
|
||||||
image: 'alpine',
|
|
||||||
system: true,
|
|
||||||
entrypoint: 'true',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Cbor,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '10m',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
'auto-configure': {
|
|
||||||
type: 'docker',
|
|
||||||
image: 'alpine',
|
|
||||||
system: true,
|
|
||||||
entrypoint: 'cat',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Cbor,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '10m',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'os-version': '0.4.0',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BitcoinDep: DependencyMetadata = {
|
export const BitcoinDep: DependencyMetadata = {
|
||||||
@@ -1993,14 +1506,9 @@ export module Mock {
|
|||||||
|
|
||||||
export const bitcoind: PackageDataEntry = {
|
export const bitcoind: PackageDataEntry = {
|
||||||
state: PackageState.Installed,
|
state: PackageState.Installed,
|
||||||
'static-files': {
|
|
||||||
license: '/public/package-data/bitcoind/0.20.0/LICENSE.md',
|
|
||||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
|
||||||
instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md',
|
|
||||||
},
|
|
||||||
manifest: MockManifestBitcoind,
|
manifest: MockManifestBitcoind,
|
||||||
|
icon: '/assets/img/service-icons/bitcoind.png',
|
||||||
installed: {
|
installed: {
|
||||||
manifest: MockManifestBitcoind,
|
|
||||||
'last-backup': null,
|
'last-backup': null,
|
||||||
status: {
|
status: {
|
||||||
configured: true,
|
configured: true,
|
||||||
@@ -2011,35 +1519,62 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
'dependency-errors': {},
|
'dependency-errors': {},
|
||||||
},
|
},
|
||||||
'interface-addresses': {
|
'address-info': {
|
||||||
ui: {
|
|
||||||
'tor-address': 'bitcoind-ui-address.onion',
|
|
||||||
'lan-address': 'bitcoind-ui-address.local',
|
|
||||||
},
|
|
||||||
rpc: {
|
rpc: {
|
||||||
'tor-address': 'bitcoind-rpc-address.onion',
|
name: 'Bitcoin RPC',
|
||||||
'lan-address': 'bitcoind-rpc-address.local',
|
description: `Bitcoin's RPC interface`,
|
||||||
|
addresses: [
|
||||||
|
'http://bitcoind-rpc-address.onion',
|
||||||
|
'https://bitcoind-rpc-address.local',
|
||||||
|
'https://192.168.1.1:8332',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
p2p: {
|
p2p: {
|
||||||
'tor-address': 'bitcoind-p2p-address.onion',
|
name: 'Bitcoin P2P',
|
||||||
'lan-address': 'bitcoind-p2p-address.local',
|
description: `Bitcoin's P2P interface`,
|
||||||
|
addresses: [
|
||||||
|
'bitcoin://bitcoind-rpc-address.onion',
|
||||||
|
'bitcoin://192.168.1.1:8333',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'current-dependencies': {},
|
'current-dependencies': {},
|
||||||
'dependency-info': {},
|
'dependency-info': {},
|
||||||
'marketplace-url': 'https://registry.start9.com/',
|
'marketplace-url': 'https://registry.start9.com/',
|
||||||
'developer-key': 'developer-key',
|
'developer-key': 'developer-key',
|
||||||
|
'has-config': true,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
resync: {
|
||||||
|
name: 'Resync Blockchain',
|
||||||
|
description: 'Use this to resync the Bitcoin blockchain from genesis',
|
||||||
|
warning: 'This will take a couple of days.',
|
||||||
|
disabled: null,
|
||||||
|
group: null,
|
||||||
|
'input-spec': {
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
name: 'Re-sync Reason',
|
||||||
|
description: 'Your reason for re-syncing. Why are you doing this?',
|
||||||
|
placeholder: null,
|
||||||
|
nullable: false,
|
||||||
|
masked: false,
|
||||||
|
pattern: '^[a-zA-Z]+$',
|
||||||
|
'pattern-description': 'Must contain only letters.',
|
||||||
|
textarea: false,
|
||||||
|
warning: null,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'install-progress': undefined,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bitcoinProxy: PackageDataEntry = {
|
export const bitcoinProxy: PackageDataEntry = {
|
||||||
state: PackageState.Installed,
|
state: PackageState.Installed,
|
||||||
'static-files': {
|
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
||||||
license: '/public/package-data/btc-rpc-proxy/0.20.0/LICENSE.md',
|
|
||||||
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
|
||||||
instructions: '/public/package-data/btc-rpc-proxy/0.20.0/INSTRUCTIONS.md',
|
|
||||||
},
|
|
||||||
manifest: MockManifestBitcoinProxy,
|
manifest: MockManifestBitcoinProxy,
|
||||||
installed: {
|
installed: {
|
||||||
'last-backup': null,
|
'last-backup': null,
|
||||||
@@ -2050,11 +1585,15 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
'dependency-errors': {},
|
'dependency-errors': {},
|
||||||
},
|
},
|
||||||
manifest: MockManifestBitcoinProxy,
|
'address-info': {
|
||||||
'interface-addresses': {
|
|
||||||
rpc: {
|
rpc: {
|
||||||
'tor-address': 'bitcoinproxy-rpc-address.onion',
|
name: 'Proxy RPC addresses',
|
||||||
'lan-address': 'bitcoinproxy-rpc-address.local',
|
description: `Use these addresses to access Proxy's RPC interface`,
|
||||||
|
addresses: [
|
||||||
|
'http://bitcoinproxy-rpc-address.onion',
|
||||||
|
'https://bitcoinproxy-rpc-address.local',
|
||||||
|
],
|
||||||
|
ui: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'current-dependencies': {
|
'current-dependencies': {
|
||||||
@@ -2064,23 +1603,20 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
'dependency-info': {
|
'dependency-info': {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
manifest: Mock.MockManifestBitcoind,
|
title: 'Bitcoin Core',
|
||||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'marketplace-url': 'https://registry.start9.com/',
|
'marketplace-url': 'https://registry.start9.com/',
|
||||||
'developer-key': 'developer-key',
|
'developer-key': 'developer-key',
|
||||||
|
'has-config': true,
|
||||||
},
|
},
|
||||||
'install-progress': undefined,
|
actions: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const lnd: PackageDataEntry = {
|
export const lnd: PackageDataEntry = {
|
||||||
state: PackageState.Installed,
|
state: PackageState.Installed,
|
||||||
'static-files': {
|
icon: '/assets/img/service-icons/lnd.png',
|
||||||
license: '/public/package-data/lnd/0.11.0/LICENSE.md',
|
|
||||||
icon: '/assets/img/service-icons/lnd.png',
|
|
||||||
instructions: '/public/package-data/lnd/0.11.0/INSTRUCTIONS.md',
|
|
||||||
},
|
|
||||||
manifest: MockManifestLnd,
|
manifest: MockManifestLnd,
|
||||||
installed: {
|
installed: {
|
||||||
'last-backup': null,
|
'last-backup': null,
|
||||||
@@ -2095,15 +1631,26 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
manifest: MockManifestLnd,
|
'address-info': {
|
||||||
'interface-addresses': {
|
ui: {
|
||||||
rpc: {
|
name: 'Web UI',
|
||||||
'tor-address': 'lnd-rpc-address.onion',
|
description: 'The browser web interface for LND',
|
||||||
'lan-address': 'lnd-rpc-address.local',
|
addresses: [
|
||||||
|
'http://lnd-ui-address.onion',
|
||||||
|
'https://lnd-ui-address.local',
|
||||||
|
'https://192.168.1.1:3449',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
grpc: {
|
grpc: {
|
||||||
'tor-address': 'lnd-grpc-address.onion',
|
name: 'gRPC',
|
||||||
'lan-address': 'lnd-grpc-address.local',
|
description: 'For connecting to LND gRPC interface',
|
||||||
|
addresses: [
|
||||||
|
'http://lnd-grpc-address.onion',
|
||||||
|
'https://lnd-grpc-address.local',
|
||||||
|
'https://192.168.1.1:3449',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'current-dependencies': {
|
'current-dependencies': {
|
||||||
@@ -2116,23 +1663,24 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
'dependency-info': {
|
'dependency-info': {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
manifest: Mock.MockManifestBitcoind,
|
title: 'Bitcoin Core',
|
||||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||||
},
|
},
|
||||||
'btc-rpc-proxy': {
|
'btc-rpc-proxy': {
|
||||||
manifest: Mock.MockManifestBitcoinProxy,
|
title: 'Bitcoin Proxy',
|
||||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'marketplace-url': 'https://registry.start9.com/',
|
'marketplace-url': 'https://registry.start9.com/',
|
||||||
'developer-key': 'developer-key',
|
'developer-key': 'developer-key',
|
||||||
|
'has-config': true,
|
||||||
},
|
},
|
||||||
'install-progress': undefined,
|
actions: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocalPkgs: { [key: string]: PackageDataEntry } = {
|
export const LocalPkgs: { [key: string]: PackageDataEntry } = {
|
||||||
bitcoind: bitcoind,
|
bitcoind,
|
||||||
'btc-rpc-proxy': bitcoinProxy,
|
'btc-rpc-proxy': bitcoinProxy,
|
||||||
lnd: lnd,
|
lnd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Dump, Revision } from 'patch-db-client'
|
import { Dump, Revision } from 'patch-db-client'
|
||||||
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
|
import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
|
||||||
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
|
||||||
import { InputSpec } from 'start-sdk/types/config-types'
|
import { InputSpec } from 'start-sdk/types/config-types'
|
||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
DependencyError,
|
DependencyError,
|
||||||
Manifest,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
ServerStatus,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { CifsBackupTarget, RR } from './api.types'
|
import { CifsBackupTarget, RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
@@ -999,11 +998,11 @@ export class MockApiService extends ApiService {
|
|||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch3: Operation<ServerStatus>[] = [
|
const patch3: Operation<boolean>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/server-info/status',
|
path: '/server-info/status-info/updated',
|
||||||
value: ServerStatus.Updated,
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
op: PatchOp.REMOVE,
|
op: PatchOp.REMOVE,
|
||||||
@@ -1011,16 +1010,6 @@ export class MockApiService extends ApiService {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch3)
|
this.mockRevision(patch3)
|
||||||
// quickly revert server to "running" for continued testing
|
|
||||||
await pauseFor(100)
|
|
||||||
const patch4 = [
|
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: '/server-info/status',
|
|
||||||
value: ServerStatus.Running,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch4)
|
|
||||||
// set patch indicating update is complete
|
// set patch indicating update is complete
|
||||||
await pauseFor(100)
|
await pauseFor(100)
|
||||||
const patch6 = [
|
const patch6 = [
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
DataModel,
|
DataModel,
|
||||||
DependencyErrorType,
|
DependencyErrorType,
|
||||||
DockerIoFormat,
|
|
||||||
HealthResult,
|
HealthResult,
|
||||||
Manifest,
|
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { Mock } from './api.fixures'
|
|
||||||
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
|
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
|
||||||
|
|
||||||
export const mockPatchData: DataModel = {
|
export const mockPatchData: DataModel = {
|
||||||
@@ -68,6 +65,7 @@ export const mockPatchData: DataModel = {
|
|||||||
'backup-progress': null,
|
'backup-progress': null,
|
||||||
updated: false,
|
updated: false,
|
||||||
'update-progress': null,
|
'update-progress': null,
|
||||||
|
'shutting-down': false,
|
||||||
},
|
},
|
||||||
hostname: 'random-words',
|
hostname: 'random-words',
|
||||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||||
@@ -78,11 +76,7 @@ export const mockPatchData: DataModel = {
|
|||||||
'package-data': {
|
'package-data': {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
state: PackageState.Installed,
|
state: PackageState.Installed,
|
||||||
'static-files': {
|
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||||
license: '/public/package-data/bitcoind/0.20.0/LICENSE.md',
|
|
||||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
|
||||||
instructions: '/public/package-data/bitcoind/0.20.0/INSTRUCTIONS.md',
|
|
||||||
},
|
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'bitcoind',
|
id: 'bitcoind',
|
||||||
title: 'Bitcoin Core',
|
title: 'Bitcoin Core',
|
||||||
@@ -92,15 +86,10 @@ export const mockPatchData: DataModel = {
|
|||||||
short: 'A Bitcoin full node by Bitcoin Core.',
|
short: 'A Bitcoin full node by Bitcoin Core.',
|
||||||
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
|
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
|
||||||
},
|
},
|
||||||
'release-notes': 'Taproot, Schnorr, and more.',
|
|
||||||
assets: {
|
assets: {
|
||||||
icon: 'icon.png',
|
icon: 'icon.png',
|
||||||
license: 'LICENSE.md',
|
|
||||||
instructions: 'INSTRUCTIONS.md',
|
|
||||||
docker_images: 'image.tar',
|
|
||||||
assets: './assets',
|
|
||||||
scripts: './scripts',
|
|
||||||
},
|
},
|
||||||
|
'release-notes': 'Taproot, Schnorr, and more.',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
|
'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper',
|
||||||
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
|
'upstream-repo': 'https://github.com/bitcoin/bitcoin',
|
||||||
@@ -115,308 +104,10 @@ export const mockPatchData: DataModel = {
|
|||||||
start: 'Starting Bitcoin is good for your health.',
|
start: 'Starting Bitcoin is good for your health.',
|
||||||
stop: null,
|
stop: null,
|
||||||
},
|
},
|
||||||
main: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': '.49m',
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
get: {},
|
|
||||||
set: {},
|
|
||||||
} as any,
|
|
||||||
volumes: {},
|
|
||||||
'min-os-version': '0.2.12',
|
|
||||||
interfaces: {
|
|
||||||
ui: {
|
|
||||||
name: 'Node Visualizer',
|
|
||||||
description:
|
|
||||||
'Web application for viewing information about your node and the Bitcoin network.',
|
|
||||||
ui: true,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
rpc: {
|
|
||||||
name: 'RPC',
|
|
||||||
description:
|
|
||||||
'Used by wallets to interact with your Bitcoin Core node.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
p2p: {
|
|
||||||
name: 'P2P',
|
|
||||||
description:
|
|
||||||
'Used by other Bitcoin nodes to communicate and interact with your node.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backup: {
|
|
||||||
create: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
restore: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
migrations: null,
|
|
||||||
actions: {
|
|
||||||
resync: {
|
|
||||||
name: 'Resync Blockchain',
|
|
||||||
description:
|
|
||||||
'Use this to resync the Bitcoin blockchain from genesis',
|
|
||||||
warning: 'This will take a couple of days.',
|
|
||||||
'allowed-statuses': [
|
|
||||||
PackageMainStatus.Running,
|
|
||||||
PackageMainStatus.Stopped,
|
|
||||||
],
|
|
||||||
implementation: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
'input-spec': {
|
|
||||||
reason: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Re-sync Reason',
|
|
||||||
description:
|
|
||||||
'Your reason for re-syncing. Why are you doing this?',
|
|
||||||
nullable: false,
|
|
||||||
masked: false,
|
|
||||||
pattern: '^[a-zA-Z]+$',
|
|
||||||
'pattern-description': 'Must contain only letters.',
|
|
||||||
placeholder: null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Your Name',
|
|
||||||
description: 'Tell the class your name.',
|
|
||||||
nullable: true,
|
|
||||||
masked: false,
|
|
||||||
warning: 'You may loose all your money by providing your name.',
|
|
||||||
placeholder: null,
|
|
||||||
pattern: null,
|
|
||||||
'pattern-description': null,
|
|
||||||
textarea: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
notifications: {
|
|
||||||
name: 'Notification Preferences',
|
|
||||||
type: 'list',
|
|
||||||
subtype: 'enum',
|
|
||||||
description: 'how you want to be notified',
|
|
||||||
warning: null,
|
|
||||||
range: '[1,3]',
|
|
||||||
default: ['email'],
|
|
||||||
spec: {
|
|
||||||
'value-names': {
|
|
||||||
email: 'Email',
|
|
||||||
text: 'Text',
|
|
||||||
call: 'Call',
|
|
||||||
push: 'Push',
|
|
||||||
webhook: 'Webhook',
|
|
||||||
},
|
|
||||||
values: ['email', 'text', 'call', 'push', 'webhook'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'days-ago': {
|
|
||||||
type: 'number',
|
|
||||||
name: 'Days Ago',
|
|
||||||
description: 'Number of days to re-sync.',
|
|
||||||
nullable: false,
|
|
||||||
default: 100,
|
|
||||||
range: '[0, 9999]',
|
|
||||||
integral: true,
|
|
||||||
units: null,
|
|
||||||
placeholder: null,
|
|
||||||
warning: null,
|
|
||||||
},
|
|
||||||
'top-speed': {
|
|
||||||
type: 'number',
|
|
||||||
name: 'Top Speed',
|
|
||||||
description: 'The fastest you can possibly run.',
|
|
||||||
nullable: false,
|
|
||||||
range: '[-1000, 1000]',
|
|
||||||
integral: false,
|
|
||||||
units: 'm/s',
|
|
||||||
placeholder: null,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
testnet: {
|
|
||||||
name: 'Testnet',
|
|
||||||
type: 'boolean',
|
|
||||||
description:
|
|
||||||
'<ul><li>determines whether your node is running on testnet or mainnet</li></ul><script src="fake"></script>',
|
|
||||||
warning: 'Chain will have to resync!',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
randomEnum: {
|
|
||||||
name: 'Random Enum',
|
|
||||||
type: 'enum',
|
|
||||||
'value-names': {
|
|
||||||
null: 'Null',
|
|
||||||
good: 'Good',
|
|
||||||
bad: 'Bad',
|
|
||||||
ugly: 'Ugly',
|
|
||||||
},
|
|
||||||
default: 'null',
|
|
||||||
description: 'This is not even real.',
|
|
||||||
warning: 'Be careful changing this!',
|
|
||||||
values: ['null', 'good', 'bad', 'ugly'],
|
|
||||||
},
|
|
||||||
'emergency-contact': {
|
|
||||||
name: 'Emergency Contact',
|
|
||||||
type: 'object',
|
|
||||||
description: 'The person to contact in case of emergency.',
|
|
||||||
warning: null,
|
|
||||||
spec: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Name',
|
|
||||||
description: null,
|
|
||||||
nullable: false,
|
|
||||||
masked: false,
|
|
||||||
pattern: '^[a-zA-Z]+$',
|
|
||||||
'pattern-description': 'Must contain only letters.',
|
|
||||||
placeholder: null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: 'string',
|
|
||||||
name: 'Email',
|
|
||||||
description: null,
|
|
||||||
nullable: false,
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
pattern: null,
|
|
||||||
'pattern-description': null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ips: {
|
|
||||||
name: 'Whitelist IPs',
|
|
||||||
type: 'list',
|
|
||||||
subtype: 'string',
|
|
||||||
description:
|
|
||||||
'external ip addresses that are authorized to access your Bitcoin node',
|
|
||||||
warning:
|
|
||||||
'Any IP you allow here will have RPC access to your Bitcoin node.',
|
|
||||||
range: '[1,10]',
|
|
||||||
default: ['192.168.1.1'],
|
|
||||||
spec: {
|
|
||||||
pattern: '^[0-9]{1,3}([,.][0-9]{1,3})?$',
|
|
||||||
'pattern-description': 'Must be a valid IP address',
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bitcoinNode: {
|
|
||||||
type: 'union',
|
|
||||||
default: 'internal',
|
|
||||||
tag: {
|
|
||||||
id: 'type',
|
|
||||||
'variant-names': {
|
|
||||||
internal: 'Internal',
|
|
||||||
external: 'External',
|
|
||||||
},
|
|
||||||
name: 'Bitcoin Node Settings',
|
|
||||||
description: 'The node settings',
|
|
||||||
warning: 'Careful changing this',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
internal: {
|
|
||||||
'friendly-name': {
|
|
||||||
name: 'Friendly Name',
|
|
||||||
type: 'string',
|
|
||||||
description: 'the lan address',
|
|
||||||
nullable: true,
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
pattern: null,
|
|
||||||
'pattern-description': null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
external: {
|
|
||||||
'public-domain': {
|
|
||||||
name: 'Public Domain',
|
|
||||||
type: 'string',
|
|
||||||
description: 'the public address of the node',
|
|
||||||
nullable: false,
|
|
||||||
default: 'bitcoinnode.com',
|
|
||||||
pattern: '.*',
|
|
||||||
'pattern-description': 'anything',
|
|
||||||
masked: false,
|
|
||||||
placeholder: null,
|
|
||||||
textarea: false,
|
|
||||||
warning: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies: {},
|
dependencies: {},
|
||||||
|
'os-version': '0.4.0',
|
||||||
},
|
},
|
||||||
installed: {
|
installed: {
|
||||||
manifest: {
|
|
||||||
...Mock.MockManifestBitcoind,
|
|
||||||
version: '0.20.0',
|
|
||||||
},
|
|
||||||
'last-backup': null,
|
'last-backup': null,
|
||||||
status: {
|
status: {
|
||||||
configured: true,
|
configured: true,
|
||||||
@@ -446,38 +137,43 @@ export const mockPatchData: DataModel = {
|
|||||||
'unnecessary-health-check': {
|
'unnecessary-health-check': {
|
||||||
name: 'Totally Unnecessary',
|
name: 'Totally Unnecessary',
|
||||||
result: HealthResult.Disabled,
|
result: HealthResult.Disabled,
|
||||||
|
reason: 'You disabled this on purpose',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'dependency-errors': {},
|
'dependency-errors': {},
|
||||||
},
|
},
|
||||||
'interface-addresses': {
|
'address-info': {
|
||||||
ui: {
|
|
||||||
'tor-address': 'bitcoind-ui-address.onion',
|
|
||||||
'lan-address': 'bitcoind-ui-address.local',
|
|
||||||
},
|
|
||||||
rpc: {
|
rpc: {
|
||||||
'tor-address': 'bitcoind-rpc-address.onion',
|
name: 'Bitcoin RPC',
|
||||||
'lan-address': 'bitcoind-rpc-address.local',
|
description: `Bitcoin's RPC interface`,
|
||||||
|
addresses: [
|
||||||
|
'http://bitcoind-rpc-address.onion',
|
||||||
|
'https://bitcoind-rpc-address.local',
|
||||||
|
'https://192.168.1.1:8332',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
p2p: {
|
p2p: {
|
||||||
'tor-address': 'bitcoind-p2p-address.onion',
|
name: 'Bitcoin P2P',
|
||||||
'lan-address': 'bitcoind-p2p-address.local',
|
description: `Bitcoin's P2P interface`,
|
||||||
|
addresses: [
|
||||||
|
'bitcoin://bitcoind-rpc-address.onion',
|
||||||
|
'bitcoin://192.168.1.1:8333',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'current-dependencies': {},
|
'current-dependencies': {},
|
||||||
'dependency-info': {},
|
'dependency-info': {},
|
||||||
'marketplace-url': 'https://registry.start9.com/',
|
'marketplace-url': 'https://registry.start9.com/',
|
||||||
'developer-key': 'developer-key',
|
'developer-key': 'developer-key',
|
||||||
|
'has-config': true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
lnd: {
|
lnd: {
|
||||||
state: PackageState.Installed,
|
state: PackageState.Installed,
|
||||||
'static-files': {
|
icon: '/assets/img/service-icons/lnd.png',
|
||||||
license: '/public/package-data/lnd/0.11.1/LICENSE.md',
|
|
||||||
icon: '/assets/img/service-icons/lnd.png',
|
|
||||||
instructions: '/public/package-data/lnd/0.11.1/INSTRUCTIONS.md',
|
|
||||||
},
|
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'lnd',
|
id: 'lnd',
|
||||||
title: 'Lightning Network Daemon',
|
title: 'Lightning Network Daemon',
|
||||||
@@ -486,15 +182,10 @@ export const mockPatchData: DataModel = {
|
|||||||
short: 'A bolt spec compliant client.',
|
short: 'A bolt spec compliant client.',
|
||||||
long: 'More info about LND. More info about LND. More info about LND.',
|
long: 'More info about LND. More info about LND. More info about LND.',
|
||||||
},
|
},
|
||||||
'release-notes': 'Dual funded channels!',
|
|
||||||
assets: {
|
assets: {
|
||||||
icon: 'icon.png',
|
icon: 'icon.png',
|
||||||
license: 'LICENSE.md',
|
|
||||||
instructions: 'INSTRUCTIONS.md',
|
|
||||||
docker_images: 'image.tar',
|
|
||||||
assets: './assets',
|
|
||||||
scripts: './scripts',
|
|
||||||
},
|
},
|
||||||
|
'release-notes': 'Dual funded channels!',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
|
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
|
||||||
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
|
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
|
||||||
@@ -509,104 +200,6 @@ export const mockPatchData: DataModel = {
|
|||||||
start: 'Starting LND is good for your health.',
|
start: 'Starting LND is good for your health.',
|
||||||
stop: null,
|
stop: null,
|
||||||
},
|
},
|
||||||
main: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': '0.5s',
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
get: null,
|
|
||||||
set: null,
|
|
||||||
},
|
|
||||||
volumes: {},
|
|
||||||
'min-os-version': '0.2.12',
|
|
||||||
interfaces: {
|
|
||||||
rpc: {
|
|
||||||
name: 'RPC interface',
|
|
||||||
description: 'Good for connecting to your node at a distance.',
|
|
||||||
ui: true,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {
|
|
||||||
'44': {
|
|
||||||
ssl: true,
|
|
||||||
mapping: 33,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
grpc: {
|
|
||||||
name: 'GRPC',
|
|
||||||
description: 'Certain wallet use grpc.',
|
|
||||||
ui: false,
|
|
||||||
'tor-config': {
|
|
||||||
'port-mapping': {},
|
|
||||||
},
|
|
||||||
'lan-config': {
|
|
||||||
'66': {
|
|
||||||
ssl: true,
|
|
||||||
mapping: 55,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
protocols: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
backup: {
|
|
||||||
create: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
restore: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
migrations: null,
|
|
||||||
actions: {
|
|
||||||
resync: {
|
|
||||||
name: 'Resync Network Graph',
|
|
||||||
description: 'Your node will resync its network graph.',
|
|
||||||
warning: 'This will take a couple hours.',
|
|
||||||
'allowed-statuses': [PackageMainStatus.Running],
|
|
||||||
implementation: {
|
|
||||||
type: 'docker',
|
|
||||||
image: '',
|
|
||||||
system: true,
|
|
||||||
entrypoint: '',
|
|
||||||
args: [],
|
|
||||||
mounts: {},
|
|
||||||
'io-format': DockerIoFormat.Yaml,
|
|
||||||
inject: false,
|
|
||||||
'shm-size': '',
|
|
||||||
'sigterm-timeout': null,
|
|
||||||
},
|
|
||||||
'input-spec': null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
version: '=0.21.0',
|
version: '=0.21.0',
|
||||||
@@ -615,7 +208,6 @@ export const mockPatchData: DataModel = {
|
|||||||
type: 'opt-out',
|
type: 'opt-out',
|
||||||
how: 'You can use an external node from your server if you prefer.',
|
how: 'You can use an external node from your server if you prefer.',
|
||||||
},
|
},
|
||||||
config: null,
|
|
||||||
},
|
},
|
||||||
'btc-rpc-proxy': {
|
'btc-rpc-proxy': {
|
||||||
version: '>=0.2.2',
|
version: '>=0.2.2',
|
||||||
@@ -625,9 +217,9 @@ export const mockPatchData: DataModel = {
|
|||||||
type: 'opt-in',
|
type: 'opt-in',
|
||||||
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
|
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
|
||||||
},
|
},
|
||||||
config: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'os-version': '0.4.0',
|
||||||
},
|
},
|
||||||
installed: {
|
installed: {
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -647,14 +239,26 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'interface-addresses': {
|
'address-info': {
|
||||||
rpc: {
|
ui: {
|
||||||
'tor-address': 'lnd-rpc-address.onion',
|
name: 'Web UI',
|
||||||
'lan-address': 'lnd-rpc-address.local',
|
description: 'The browser web interface for LND',
|
||||||
|
addresses: [
|
||||||
|
'http://lnd-ui-address.onion',
|
||||||
|
'https://lnd-ui-address.local',
|
||||||
|
'https://192.168.1.1:3449',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
grpc: {
|
grpc: {
|
||||||
'tor-address': 'lnd-grpc-address.onion',
|
name: 'gRPC',
|
||||||
'lan-address': 'lnd-grpc-address.local',
|
description: 'For connecting to LND gRPC interface',
|
||||||
|
addresses: [
|
||||||
|
'http://lnd-grpc-address.onion',
|
||||||
|
'https://lnd-grpc-address.local',
|
||||||
|
'https://192.168.1.1:3449',
|
||||||
|
],
|
||||||
|
ui: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'current-dependencies': {
|
'current-dependencies': {
|
||||||
@@ -667,20 +271,17 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
'dependency-info': {
|
'dependency-info': {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
manifest: {
|
title: 'Bitcoin Core',
|
||||||
title: 'Bitcoin Core',
|
|
||||||
} as Manifest,
|
|
||||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||||
},
|
},
|
||||||
'btc-rpc-proxy': {
|
'btc-rpc-proxy': {
|
||||||
manifest: {
|
title: 'Bitcoin Proxy',
|
||||||
title: 'Bitcoin Proxy',
|
|
||||||
} as Manifest,
|
|
||||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'marketplace-url': 'https://registry.start9.com/',
|
'marketplace-url': 'https://registry.start9.com/',
|
||||||
'developer-key': 'developer-key',
|
'developer-key': 'developer-key',
|
||||||
|
'has-config': true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import { DOCUMENT } from '@angular/common'
|
|||||||
import { Inject, Injectable } from '@angular/core'
|
import { Inject, Injectable } from '@angular/core'
|
||||||
import { WorkspaceConfig } from '@start9labs/shared'
|
import { WorkspaceConfig } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
InterfaceDef,
|
InstalledPackageInfo,
|
||||||
PackageDataEntry,
|
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -52,56 +50,12 @@ export class ConfigService {
|
|||||||
isSecure(): boolean {
|
isSecure(): boolean {
|
||||||
return window.isSecureContext || this.isTor()
|
return window.isSecureContext || this.isTor()
|
||||||
}
|
}
|
||||||
|
|
||||||
isLaunchable(
|
|
||||||
state: PackageState,
|
|
||||||
status: PackageMainStatus,
|
|
||||||
interfaces: Record<string, InterfaceDef>,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
state === PackageState.Installed &&
|
|
||||||
status === PackageMainStatus.Running &&
|
|
||||||
hasUi(interfaces)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
launchableURL(pkg: PackageDataEntry): string {
|
|
||||||
if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) {
|
|
||||||
return `https://${lanUiAddress(pkg)}`
|
|
||||||
} else {
|
|
||||||
return `http://${torUiAddress(pkg)}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTorUi(interfaces: Record<string, InterfaceDef>): boolean {
|
export function hasUi(
|
||||||
const int = getUiInterfaceValue(interfaces)
|
addressInfo: InstalledPackageInfo['address-info'],
|
||||||
return !!int?.['tor-config']
|
): boolean {
|
||||||
}
|
return !!Object.values(addressInfo).find(a => a.ui)
|
||||||
|
|
||||||
export function hasLanUi(interfaces: Record<string, InterfaceDef>): boolean {
|
|
||||||
const int = getUiInterfaceValue(interfaces)
|
|
||||||
return !!int?.['lan-config']
|
|
||||||
}
|
|
||||||
|
|
||||||
export function torUiAddress({
|
|
||||||
manifest,
|
|
||||||
installed,
|
|
||||||
}: PackageDataEntry): string {
|
|
||||||
const key = getUiInterfaceKey(manifest.interfaces)
|
|
||||||
return installed ? installed['interface-addresses'][key]['tor-address'] : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function lanUiAddress({
|
|
||||||
manifest,
|
|
||||||
installed,
|
|
||||||
}: PackageDataEntry): string {
|
|
||||||
const key = getUiInterfaceKey(manifest.interfaces)
|
|
||||||
return installed ? installed['interface-addresses'][key]['lan-address'] : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasUi(interfaces: Record<string, InterfaceDef>): boolean {
|
|
||||||
return hasTorUi(interfaces) || hasLanUi(interfaces)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeProtocol(str: string): string {
|
export function removeProtocol(str: string): string {
|
||||||
@@ -113,15 +67,3 @@ export function removeProtocol(str: string): string {
|
|||||||
export function removePort(str: string): string {
|
export function removePort(str: string): string {
|
||||||
return str.split(':')[0]
|
return str.split(':')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUiInterfaceKey(
|
|
||||||
interfaces: Record<string, InterfaceDef>,
|
|
||||||
): string {
|
|
||||||
return Object.keys(interfaces).find(key => interfaces[key].ui) || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUiInterfaceValue(
|
|
||||||
interfaces: Record<string, InterfaceDef>,
|
|
||||||
): InterfaceDef | null {
|
|
||||||
return Object.values(interfaces).find(i => i.ui) || null
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InputSpec } from 'start-sdk/types/config-types'
|
import { InputSpec } from 'start-sdk/types/config-types'
|
||||||
import { Url } from '@start9labs/shared'
|
import { Url } from '@start9labs/shared'
|
||||||
import { MarketplaceManifest } from '@start9labs/marketplace'
|
import { Manifest } from '@start9labs/marketplace'
|
||||||
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
|
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'
|
||||||
|
|
||||||
export interface DataModel {
|
export interface DataModel {
|
||||||
@@ -11,7 +11,7 @@ export interface DataModel {
|
|||||||
|
|
||||||
export interface UIData {
|
export interface UIData {
|
||||||
name: string | null
|
name: string | null
|
||||||
'ack-welcome': string // eOS emver
|
'ack-welcome': string // emver
|
||||||
marketplace: UIMarketplaceData
|
marketplace: UIMarketplaceData
|
||||||
dev: DevData
|
dev: DevData
|
||||||
gaming: {
|
gaming: {
|
||||||
@@ -95,176 +95,98 @@ export interface ServerStatusInfo {
|
|||||||
}
|
}
|
||||||
updated: boolean
|
updated: boolean
|
||||||
'update-progress': { size: number | null; downloaded: number } | null
|
'update-progress': { size: number | null; downloaded: number } | null
|
||||||
}
|
'shutting-down': boolean
|
||||||
|
|
||||||
export enum ServerStatus {
|
|
||||||
Running = 'running',
|
|
||||||
Updated = 'updated',
|
|
||||||
BackingUp = 'backing-up',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PackageDataEntry {
|
export interface PackageDataEntry {
|
||||||
state: PackageState
|
state: PackageState
|
||||||
'static-files': {
|
|
||||||
license: Url
|
|
||||||
instructions: Url
|
|
||||||
icon: Url
|
|
||||||
}
|
|
||||||
manifest: Manifest
|
manifest: Manifest
|
||||||
installed?: InstalledPackageDataEntry // exists when: installed, updating
|
icon: string
|
||||||
'install-progress'?: InstallProgress // exists when: installing, updating
|
installed?: InstalledPackageInfo // when: installed
|
||||||
|
actions?: Record<string, Action> // when: installed
|
||||||
|
'install-progress'?: InstallProgress // when: installing, updating, restoring
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export type PackageDataEntry =
|
||||||
|
// | PackageDataEntryInstalled
|
||||||
|
// | PackageDataEntryNeedsUpdate
|
||||||
|
// | PackageDataEntryRemoving
|
||||||
|
// | PackageDataEntryRestoring
|
||||||
|
// | PackageDataEntryUpdating
|
||||||
|
// | PackageDataEntryInstalling
|
||||||
|
|
||||||
|
// export type PackageDataEntryBase = {
|
||||||
|
// manifest: Manifest
|
||||||
|
// icon: Url
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PackageDataEntryInstalled extends PackageDataEntryBase {
|
||||||
|
// state: PackageState.Installed
|
||||||
|
// installed: InstalledPackageInfo
|
||||||
|
// actions: Record<string, Action>
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PackageDataEntryNeedsUpdate extends PackageDataEntryBase {
|
||||||
|
// state: PackageState.NeedsUpdate
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PackageDataEntryRemoving extends PackageDataEntryBase {
|
||||||
|
// state: PackageState.Removing
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PackageDataEntryRestoring extends PackageDataEntryBase {
|
||||||
|
// state: PackageState.Restoring
|
||||||
|
// 'install-progress': InstallProgress
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PackageDataEntryUpdating extends PackageDataEntryBase {
|
||||||
|
// state: PackageState.Updating
|
||||||
|
// 'install-progress': InstallProgress
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface PackageDataEntryInstalling extends PackageDataEntryBase {
|
||||||
|
// state: PackageState.Installing
|
||||||
|
// 'install-progress': InstallProgress
|
||||||
|
// }
|
||||||
|
|
||||||
export enum PackageState {
|
export enum PackageState {
|
||||||
Installing = 'installing',
|
Installing = 'installing',
|
||||||
Installed = 'installed',
|
Installed = 'installed',
|
||||||
Updating = 'updating',
|
Updating = 'updating',
|
||||||
Removing = 'removing',
|
Removing = 'removing',
|
||||||
Restoring = 'restoring',
|
Restoring = 'restoring',
|
||||||
|
NeedsUpdate = 'needs-update',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstalledPackageDataEntry {
|
export interface InstalledPackageInfo {
|
||||||
status: Status
|
status: Status
|
||||||
manifest: Manifest
|
|
||||||
'last-backup': string | null
|
'last-backup': string | null
|
||||||
'current-dependencies': { [id: string]: CurrentDependencyInfo }
|
'current-dependencies': Record<string, CurrentDependencyInfo>
|
||||||
'dependency-info': {
|
'dependency-info': Record<string, { title: string; icon: Url }>
|
||||||
[id: string]: {
|
'address-info': Record<string, AddressInfo>
|
||||||
manifest: Manifest
|
|
||||||
icon: Url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'interface-addresses': {
|
|
||||||
[id: string]: { 'tor-address': string; 'lan-address': string }
|
|
||||||
}
|
|
||||||
'marketplace-url': string | null
|
'marketplace-url': string | null
|
||||||
'developer-key': string
|
'developer-key': string
|
||||||
|
'has-config': boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentDependencyInfo {
|
export interface CurrentDependencyInfo {
|
||||||
'health-checks': string[] // array of health check IDs
|
'health-checks': string[] // array of health check IDs
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
|
export interface AddressInfo {
|
||||||
assets: {
|
|
||||||
license: string // filename
|
|
||||||
instructions: string // filename
|
|
||||||
icon: string // filename
|
|
||||||
docker_images: string // filename
|
|
||||||
assets: string // path to assets folder
|
|
||||||
scripts: string // path to scripts folder
|
|
||||||
}
|
|
||||||
main: ActionImpl
|
|
||||||
config: ConfigActions | null
|
|
||||||
volumes: Record<string, Volume>
|
|
||||||
'min-os-version': string
|
|
||||||
interfaces: Record<string, InterfaceDef>
|
|
||||||
backup: BackupActions
|
|
||||||
migrations: Migrations | null
|
|
||||||
actions: Record<string, Action>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DependencyConfig {
|
|
||||||
check: ActionImpl
|
|
||||||
'auto-configure': ActionImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActionImpl {
|
|
||||||
type: 'docker'
|
|
||||||
image: string
|
|
||||||
system: boolean
|
|
||||||
entrypoint: string
|
|
||||||
args: string[]
|
|
||||||
mounts: { [id: string]: string }
|
|
||||||
'io-format': DockerIoFormat | null
|
|
||||||
inject: boolean
|
|
||||||
'shm-size': string
|
|
||||||
'sigterm-timeout': string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum DockerIoFormat {
|
|
||||||
Json = 'json',
|
|
||||||
Yaml = 'yaml',
|
|
||||||
Cbor = 'cbor',
|
|
||||||
Toml = 'toml',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigActions {
|
|
||||||
get: ActionImpl | null
|
|
||||||
set: ActionImpl | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Volume = VolumeData
|
|
||||||
|
|
||||||
export interface VolumeData {
|
|
||||||
type: VolumeType.Data
|
|
||||||
readonly: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VolumeAssets {
|
|
||||||
type: VolumeType.Assets
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VolumePointer {
|
|
||||||
type: VolumeType.Pointer
|
|
||||||
'package-id': string
|
|
||||||
'volume-id': string
|
|
||||||
path: string
|
|
||||||
readonly: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VolumeCertificate {
|
|
||||||
type: VolumeType.Certificate
|
|
||||||
'interface-id': string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VolumeBackup {
|
|
||||||
type: VolumeType.Backup
|
|
||||||
readonly: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VolumeType {
|
|
||||||
Data = 'data',
|
|
||||||
Assets = 'assets',
|
|
||||||
Pointer = 'pointer',
|
|
||||||
Certificate = 'certificate',
|
|
||||||
Backup = 'backup',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InterfaceDef {
|
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
'tor-config': TorConfig | null
|
addresses: Url[]
|
||||||
'lan-config': LanConfig | null
|
|
||||||
ui: boolean
|
ui: boolean
|
||||||
protocols: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TorConfig {
|
|
||||||
'port-mapping': { [port: number]: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LanConfig = {
|
|
||||||
[port: number]: { ssl: boolean; mapping: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackupActions {
|
|
||||||
create: ActionImpl
|
|
||||||
restore: ActionImpl
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Migrations {
|
|
||||||
from: { [versionRange: string]: ActionImpl }
|
|
||||||
to: { [versionRange: string]: ActionImpl }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
warning: string | null
|
warning: string | null
|
||||||
implementation: ActionImpl
|
disabled: string | null
|
||||||
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
|
|
||||||
'input-spec': InputSpec | null
|
'input-spec': InputSpec | null
|
||||||
|
group: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Status {
|
export interface Status {
|
||||||
@@ -280,6 +202,7 @@ export type MainStatus =
|
|||||||
| MainStatusRunning
|
| MainStatusRunning
|
||||||
| MainStatusBackingUp
|
| MainStatusBackingUp
|
||||||
| MainStatusRestarting
|
| MainStatusRestarting
|
||||||
|
| MainStatusConfiguring
|
||||||
|
|
||||||
export interface MainStatusStopped {
|
export interface MainStatusStopped {
|
||||||
status: PackageMainStatus.Stopped
|
status: PackageMainStatus.Stopped
|
||||||
@@ -301,13 +224,16 @@ export interface MainStatusRunning {
|
|||||||
|
|
||||||
export interface MainStatusBackingUp {
|
export interface MainStatusBackingUp {
|
||||||
status: PackageMainStatus.BackingUp
|
status: PackageMainStatus.BackingUp
|
||||||
started: string | null // UTC date string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MainStatusRestarting {
|
export interface MainStatusRestarting {
|
||||||
status: PackageMainStatus.Restarting
|
status: PackageMainStatus.Restarting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MainStatusConfiguring {
|
||||||
|
status: PackageMainStatus.Configuring
|
||||||
|
}
|
||||||
|
|
||||||
export enum PackageMainStatus {
|
export enum PackageMainStatus {
|
||||||
Starting = 'starting',
|
Starting = 'starting',
|
||||||
Running = 'running',
|
Running = 'running',
|
||||||
@@ -315,6 +241,7 @@ export enum PackageMainStatus {
|
|||||||
Stopped = 'stopped',
|
Stopped = 'stopped',
|
||||||
BackingUp = 'backing-up',
|
BackingUp = 'backing-up',
|
||||||
Restarting = 'restarting',
|
Restarting = 'restarting',
|
||||||
|
Configuring = 'configuring',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HealthCheckResult = { name: string } & (
|
export type HealthCheckResult = { name: string } & (
|
||||||
@@ -339,6 +266,7 @@ export interface HealthCheckResultStarting {
|
|||||||
|
|
||||||
export interface HealthCheckResultDisabled {
|
export interface HealthCheckResultDisabled {
|
||||||
result: HealthResult.Disabled
|
result: HealthResult.Disabled
|
||||||
|
reason: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HealthCheckResultSuccess {
|
export interface HealthCheckResultSuccess {
|
||||||
@@ -370,7 +298,6 @@ export enum DependencyErrorType {
|
|||||||
IncorrectVersion = 'incorrect-version',
|
IncorrectVersion = 'incorrect-version',
|
||||||
ConfigUnsatisfied = 'config-unsatisfied',
|
ConfigUnsatisfied = 'config-unsatisfied',
|
||||||
HealthChecksFailed = 'health-checks-failed',
|
HealthChecksFailed = 'health-checks-failed',
|
||||||
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
|
||||||
Transitive = 'transitive',
|
Transitive = 'transitive',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isEmptyObject } from '@start9labs/shared'
|
import { isEmptyObject } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
InstalledPackageDataEntry,
|
InstalledPackageInfo,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
@@ -38,7 +38,7 @@ function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDependencyStatus(
|
function getDependencyStatus(
|
||||||
installed: InstalledPackageDataEntry,
|
installed: InstalledPackageInfo,
|
||||||
): DependencyStatus | null {
|
): DependencyStatus | null {
|
||||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import { Inject, Injectable } from '@angular/core'
|
import { Inject, Injectable } from '@angular/core'
|
||||||
import { DOCUMENT } from '@angular/common'
|
import { DOCUMENT } from '@angular/common'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import { ConfigService } from './config.service'
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UiLauncherService {
|
export class UiLauncherService {
|
||||||
constructor(
|
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||||
@Inject(DOCUMENT) private readonly document: Document,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
launch(pkg: PackageDataEntry): void {
|
launch(addressInfo: InstalledPackageInfo['address-info']): void {
|
||||||
this.document.defaultView?.open(
|
const UIs = Object.values(addressInfo)
|
||||||
this.config.launchableURL(pkg),
|
.filter(info => info.ui)
|
||||||
'_blank',
|
.map(info => ({
|
||||||
'noreferrer',
|
name: info.name,
|
||||||
)
|
addresses: info.addresses,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (UIs.length === 1 && UIs[0].addresses.length === 1) {
|
||||||
|
this.document.defaultView?.open(
|
||||||
|
UIs[0].addresses[0],
|
||||||
|
'_blank',
|
||||||
|
'noreferrer',
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user