revamp manifest types

This commit is contained in:
Matt Hill
2024-03-21 17:21:37 -06:00
parent ab836c6922
commit 66b0108c51
43 changed files with 279 additions and 834 deletions

View File

@@ -23,7 +23,6 @@ import {
PackageId, PackageId,
EnsureStorePath, EnsureStorePath,
ExtractStore, ExtractStore,
DaemonReturned,
ValidIfNoStupidEscape, ValidIfNoStupidEscape,
} from "./types" } from "./types"
import * as patterns from "./util/patterns" import * as patterns from "./util/patterns"
@@ -205,7 +204,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
| Config<any, never>, | Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
metaData: Omit<ActionMetadata, "input"> & { id: string,
metadata: Omit<ActionMetadata, "input"> & {
input: Config<Type, Store> | Config<Type, never> input: Config<Type, Store> | Config<Type, never>
}, },
fn: (options: { fn: (options: {
@@ -213,8 +213,13 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
) => { ) => {
const { input, ...rest } = metaData const { input, ...rest } = metadata
return createAction<Manifest, Store, ConfigType, Type>(rest, fn, input) return createAction<Manifest, Store, ConfigType, Type>(
id,
rest,
fn,
input,
)
}, },
getSystemSmtp: <E extends Effects>(effects: E) => getSystemSmtp: <E extends Effects>(effects: E) =>
removeConstType<E>()(new GetSystemSmtp(effects)), removeConstType<E>()(new GetSystemSmtp(effects)),
@@ -236,7 +241,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
| Config<any, never>, | Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
metaData: (options: { id: string,
metadata: (options: {
effects: Effects effects: Effects
}) => MaybePromise<Omit<ActionMetadata, "input">>, }) => MaybePromise<Omit<ActionMetadata, "input">>,
fn: (options: { fn: (options: {
@@ -246,7 +252,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
input: Config<Type, Store> | Config<Type, never>, input: Config<Type, Store> | Config<Type, never>,
) => { ) => {
return createAction<Manifest, Store, ConfigType, Type>( return createAction<Manifest, Store, ConfigType, Type>(
metaData, id,
metadata,
fn, fn,
input, input,
) )

View File

@@ -15,7 +15,8 @@ export class CreatedAction<
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
> { > {
private constructor( private constructor(
public readonly myMetaData: MaybeFn< public readonly id: string,
public readonly myMetadata: MaybeFn<
Manifest, Manifest,
Store, Store,
Omit<ActionMetadata, "input"> Omit<ActionMetadata, "input">
@@ -37,12 +38,14 @@ export class CreatedAction<
| Config<any, never>, | Config<any, never>,
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
metaData: MaybeFn<Manifest, Store, Omit<ActionMetadata, "input">>, id: string,
metadata: MaybeFn<Manifest, Store, Omit<ActionMetadata, "input">>,
fn: (options: { effects: Effects; input: Type }) => Promise<ActionResult>, fn: (options: { effects: Effects; input: Type }) => Promise<ActionResult>,
inputConfig: Config<Type, Store> | Config<Type, never>, inputConfig: Config<Type, Store> | Config<Type, never>,
) { ) {
return new CreatedAction<Manifest, Store, ConfigType, Type>( return new CreatedAction<Manifest, Store, ConfigType, Type>(
metaData, id,
metadata,
fn, fn,
inputConfig as Config<Type, Store>, inputConfig as Config<Type, Store>,
) )
@@ -62,15 +65,15 @@ export class CreatedAction<
}) })
} }
async metaData(options: { effects: Effects }) { async metadata(options: { effects: Effects }) {
if (this.myMetaData instanceof Function) if (this.myMetadata instanceof Function)
return await this.myMetaData(options) return await this.myMetadata(options)
return this.myMetaData return this.myMetadata
} }
async ActionMetadata(options: { effects: Effects }): Promise<ActionMetadata> { async ActionMetadata(options: { effects: Effects }): Promise<ActionMetadata> {
return { return {
...(await this.metaData(options)), ...(await this.metadata(options)),
input: await this.input.build(options), input: await this.input.build(options),
} }
} }

View File

@@ -1,6 +1,5 @@
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { once } from "../util/once"
import { CreatedAction } from "./createAction" import { CreatedAction } from "./createAction"
export function setupActions<Manifest extends SDKManifest, Store>( export function setupActions<Manifest extends SDKManifest, Store>(
@@ -9,8 +8,7 @@ export function setupActions<Manifest extends SDKManifest, Store>(
const myActions = async (options: { effects: Effects }) => { const myActions = async (options: { effects: Effects }) => {
const actions: Record<string, CreatedAction<Manifest, Store, any>> = {} const actions: Record<string, CreatedAction<Manifest, Store, any>> = {}
for (const action of createdActions) { for (const action of createdActions) {
const actionMetadata = await action.metaData(options) actions[action.id] = action
actions[actionMetadata.id] = action
} }
return actions return actions
} }

View File

@@ -163,9 +163,10 @@ export type DaemonReturned = {
export type ActionMetadata = { export type ActionMetadata = {
name: string name: string
description: string description: string
id: string warning: string | null
input: InputSpec input: InputSpec
allowedStatuses: "only-running" | "only-stopped" | "any" | "disabled" disabled: boolean
allowedStatuses: "only-running" | "only-stopped" | "any"
/** /**
* So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions * So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions
*/ */

View File

@@ -1,6 +1,6 @@
<ion-item class="service-card" [routerLink]="['/marketplace', pkg.manifest.id]"> <ion-item class="service-card" [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start"> <ion-thumbnail slot="start">
<img alt="" [src]="pkg | mimeType | trustUrl" /> <img alt="" [src]="pkg.icon | trustUrl" />
</ion-thumbnail> </ion-thumbnail>
<ion-label> <ion-label>
<h2 class="montserrat"> <h2 class="montserrat">

View File

@@ -4,17 +4,10 @@ import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component' import { ItemComponent } from './item.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({ @NgModule({
declarations: [ItemComponent], declarations: [ItemComponent],
exports: [ItemComponent], exports: [ItemComponent],
imports: [ imports: [CommonModule, IonicModule, RouterModule, SharedPipesModule],
CommonModule,
IonicModule,
RouterModule,
SharedPipesModule,
MimeTypePipeModule,
],
}) })
export class ItemModule {} export class ItemModule {}

View File

@@ -18,15 +18,11 @@
<ion-label> <ion-label>
<h2> <h2>
{{ pkg['dependency-metadata'][dep.key].title }} {{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type"> <span *ngIf="dep.value.optional; else required">(optional)</span>
<span *ngSwitchCase="'required'">(required)</span> <ng-template #required>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span> <span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container> </ng-template>
</h2> </h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p> <p>{{ dep.value.description }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@@ -11,7 +11,6 @@ export class DependenciesComponent {
pkg!: MarketplacePkg pkg!: MarketplacePkg
getImg(key: string): string { getImg(key: string): string {
// @TODO fix when registry api is updated to include mimetype in icon url return this.pkg['dependency-metadata'][key].icon
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
} }
} }

View File

@@ -1,5 +1,5 @@
<div class="header montserrat"> <div class="header montserrat">
<img class="logo" alt="" [src]="pkg | mimeType | trustUrl" /> <img class="logo" alt="" [src]="pkg.icon | trustUrl" />
<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>

View File

@@ -8,7 +8,6 @@ import {
} from '@start9labs/shared' } from '@start9labs/shared'
import { PackageComponent } from './package.component' import { PackageComponent } from './package.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({ @NgModule({
declarations: [PackageComponent], declarations: [PackageComponent],
@@ -19,7 +18,6 @@ import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
SharedPipesModule, SharedPipesModule,
EmverPipesModule, EmverPipesModule,
TickerModule, TickerModule,
MimeTypePipeModule,
], ],
}) })
export class PackageModule {} export class PackageModule {}

View File

@@ -1,32 +0,0 @@
import { NgModule, Pipe, PipeTransform } from '@angular/core'
import { MarketplacePkg } from '../types'
@Pipe({
name: 'mimeType',
})
export class MimeTypePipe implements PipeTransform {
transform(pkg: MarketplacePkg): string {
if (pkg.manifest.assets.icon) {
switch (pkg.manifest.assets.icon.split('.').pop()) {
case 'png':
return `data:image/png;base64,${pkg.icon}`
case 'jpeg':
case 'jpg':
return `data:image/jpeg;base64,${pkg.icon}`
case 'gif':
return `data:image/gif;base64,${pkg.icon}`
case 'svg':
return `data:image/svg+xml;base64,${pkg.icon}`
default:
return `data:image/png;base64,${pkg.icon}`
}
}
return `data:image/png;base64,${pkg.icon}`
}
}
@NgModule({
declarations: [MimeTypePipe],
exports: [MimeTypePipe],
})
export class MimeTypePipeModule {}

View File

@@ -22,7 +22,6 @@ export * from './pages/show/package/package.component'
export * from './pages/show/package/package.module' export * from './pages/show/package/package.module'
export * from './pipes/filter-packages.pipe' export * from './pipes/filter-packages.pipe'
export * from './pipes/mime-type.pipe'
export * from './services/marketplace.service' export * from './services/marketplace.service'

View File

@@ -19,11 +19,13 @@ export interface StoreInfo {
categories: string[] categories: string[]
} }
export type StoreIdentityWithData = StoreData & StoreIdentity
export interface MarketplacePkg { 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': {
@@ -35,10 +37,11 @@ export interface MarketplacePkg {
export interface DependencyMetadata { export interface DependencyMetadata {
title: string title: string
icon: Url icon: Url
optional: boolean
hidden: boolean hidden: boolean
} }
export interface MarketplaceManifest<T = unknown> { export interface Manifest {
id: string id: string
title: string title: string
version: string version: string
@@ -47,12 +50,9 @@ export interface MarketplaceManifest<T = unknown> {
short: string short: string
long: string long: string
} }
assets: {
icon: string // ie. icon.png
}
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,23 +65,12 @@ 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
'has-config': boolean
} }
export interface Dependency<T> { export interface Dependency {
version: string
requirement:
| {
type: 'opt-in'
how: string
}
| {
type: 'opt-out'
how: string
}
| {
type: 'required'
}
description: string | null description: string | null
config: T optional: boolean
} }

View File

@@ -78,7 +78,7 @@ export class AppConfigPage {
this.pkg = pkg this.pkg = pkg
if (!this.pkg['state-info'].manifest.config) return if (!this.pkg['state-info'].manifest['has-config']) return
let newConfig: object | undefined let newConfig: object | undefined
let patch: Operation[] | undefined let patch: Operation[] | undefined
@@ -152,7 +152,7 @@ export class AppConfigPage {
this.saving = true this.saving = true
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patch))) {
this.dryConfigure() this.dryConfigure()
} else { } else {
this.configure() this.configure()

View File

@@ -9,9 +9,7 @@
<ion-content class="ion-padding-top with-widgets"> <ion-content class="ion-padding-top with-widgets">
<ng-container *ngIf="pkg$ | async as pkg"> <ng-container *ngIf="pkg$ | async as pkg">
<ion-item-group <ion-item-group *ngIf="pkg['state-info'].state === 'installed'">
*ngIf="pkg['state-info'].state === 'installed' && pkg['state-info'].manifest as manifest"
>
<!-- ** standard actions ** --> <!-- ** standard actions ** -->
<ion-item-divider>Standard Actions</ion-item-divider> <ion-item-divider>Standard Actions</ion-item-divider>
<app-actions-item <app-actions-item
@@ -24,11 +22,11 @@
></app-actions-item> ></app-actions-item>
<!-- ** specific actions ** --> <!-- ** specific actions ** -->
<ion-item-divider *ngIf="!(manifest.actions | empty)"> <ion-item-divider *ngIf="!(pkg.actions | empty)">
Actions for {{ manifest.title }} Actions for {{ pkg['state-info'].manifest.title }}
</ion-item-divider> </ion-item-divider>
<app-actions-item <app-actions-item
*ngFor="let action of manifest.actions | keyvalue: asIsOrder" *ngFor="let action of pkg.actions | keyvalue: asIsOrder"
[action]="{ [action]="{
name: action.value.name, name: action.value.name,
description: action.value.description, description: action.value.description,

View File

@@ -9,19 +9,17 @@ import {
} from '@ionic/angular' } from '@ionic/angular'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
Action,
DataModel, DataModel,
InstalledState,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
StateInfo,
Status, Status,
} 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 } 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 { getManifest } from 'src/app/util/get-package-data' import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
import { ActionMetadata } from '@start9labs/start-sdk/cjs/sdk/lib/types'
@Component({ @Component({
selector: 'app-actions', selector: 'app-actions',
@@ -44,19 +42,22 @@ export class AppActionsPage {
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
async handleAction(status: Status, action: { key: string; value: Action }) { async handleAction(
status: Status,
action: { key: string; value: ActionMetadata },
) {
if ( if (
status && status &&
(action.value['allowed-statuses'] as PackageMainStatus[]).includes( action.value.allowedStatuses.includes(
status.main.status, status.main.status, // @TODO
) )
) { ) {
if (!isEmptyObject(action.value['input-spec'] || {})) { if (!isEmptyObject(action.value.input || {})) {
const modal = await this.modalCtrl.create({ const modal = await this.modalCtrl.create({
component: GenericFormPage, component: GenericFormPage,
componentProps: { componentProps: {
title: action.value.name, title: action.value.name,
spec: action.value['input-spec'], spec: action.value.input,
buttons: [ buttons: [
{ {
text: 'Execute', text: 'Execute',
@@ -92,7 +93,7 @@ export class AppActionsPage {
await alert.present() await alert.present()
} }
} else { } else {
const statuses = [...action.value['allowed-statuses']] const statuses = [...action.value.allowedStatuses] // @TODO
const last = statuses.pop() const last = statuses.pop()
let statusesStr = statuses.join(', ') let statusesStr = statuses.join(', ')
let error = '' let error = ''
@@ -126,7 +127,7 @@ export class AppActionsPage {
alerts.uninstall || alerts.uninstall ||
`Uninstalling ${title} will permanently delete its data` `Uninstalling ${title} will permanently delete its data`
if (hasCurrentDeps(pkg)) { if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patch))) {
message = `${message}. Services that depend on ${title} will no longer work properly and may crash` message = `${message}. Services that depend on ${title} will no longer work properly and may crash`
} }

View File

@@ -3,7 +3,12 @@ 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 } from '@start9labs/shared' import {
EmptyPipe,
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 { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module'
@@ -52,6 +57,7 @@ const routes: Routes = [
UiPipeModule, UiPipeModule,
ResponsiveColModule, ResponsiveColModule,
StatusComponentModule, StatusComponentModule,
SharedPipesModule,
], ],
}) })
export class AppShowPageModule {} export class AppShowPageModule {}

View File

@@ -23,8 +23,8 @@
> >
<!-- ** health checks ** --> <!-- ** health checks ** -->
<app-show-health-checks <app-show-health-checks
*ngIf="status.primary === 'running'" *ngIf="pkg.status.main.status === 'running'"
[manifest]="pkg['state-info'].manifest" [healthChecks]="pkg.status.main.health"
></app-show-health-checks> ></app-show-health-checks>
<!-- ** dependencies ** --> <!-- ** dependencies ** -->
<app-show-dependencies <app-show-dependencies

View File

@@ -4,7 +4,6 @@ import { PatchDB } from 'patch-db-client'
import { import {
DataModel, DataModel,
InstallingState, InstallingState,
Manifest,
PackageDataEntry, PackageDataEntry,
UpdatingState, UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
@@ -27,6 +26,7 @@ import {
isRestoring, isRestoring,
isUpdating, isUpdating,
} from 'src/app/util/get-package-data' } from 'src/app/util/get-package-data'
import { Manifest } from '@start9labs/marketplace'
export interface DependencyInfo { export interface DependencyInfo {
id: string id: string
@@ -97,6 +97,7 @@ export class AppShowPage {
depErrors: PkgDependencyErrors, depErrors: PkgDependencyErrors,
): DependencyInfo { ): DependencyInfo {
const { errorText, fixText, fixAction } = this.getDepErrors( const { errorText, fixText, fixAction } = this.getDepErrors(
pkg,
manifest, manifest,
depId, depId,
depErrors, depErrors,
@@ -106,7 +107,7 @@ export class AppShowPage {
return { return {
id: depId, id: depId,
version: manifest.dependencies[depId].version, // do we want this version range? version: pkg['current-dependencies'][depId].versionRange, // @TODO do we want this version range?
title: depInfo?.title || depId, title: depInfo?.title || depId,
icon: depInfo?.icon || '', icon: depInfo?.icon || '',
errorText: errorText errorText: errorText
@@ -119,6 +120,7 @@ export class AppShowPage {
} }
private getDepErrors( private getDepErrors(
pkg: PackageDataEntry,
manifest: Manifest, manifest: Manifest,
depId: string, depId: string,
depErrors: PkgDependencyErrors, depErrors: PkgDependencyErrors,
@@ -133,15 +135,15 @@ export class AppShowPage {
if (depError.type === DependencyErrorType.NotInstalled) { if (depError.type === DependencyErrorType.NotInstalled) {
errorText = 'Not installed' errorText = 'Not installed'
fixText = 'Install' fixText = 'Install'
fixAction = () => this.fixDep(manifest, 'install', depId) fixAction = () => this.fixDep(pkg, manifest, 'install', depId)
} else if (depError.type === DependencyErrorType.IncorrectVersion) { } else if (depError.type === DependencyErrorType.IncorrectVersion) {
errorText = 'Incorrect version' errorText = 'Incorrect version'
fixText = 'Update' fixText = 'Update'
fixAction = () => this.fixDep(manifest, 'update', depId) fixAction = () => this.fixDep(pkg, manifest, 'update', depId)
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) { } else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config not satisfied' errorText = 'Config not satisfied'
fixText = 'Auto config' fixText = 'Auto config'
fixAction = () => this.fixDep(manifest, 'configure', depId) fixAction = () => this.fixDep(pkg, manifest, 'configure', depId)
} else if (depError.type === DependencyErrorType.NotRunning) { } else if (depError.type === DependencyErrorType.NotRunning) {
errorText = 'Not running' errorText = 'Not running'
fixText = 'Start' fixText = 'Start'
@@ -160,6 +162,7 @@ export class AppShowPage {
} }
private async fixDep( private async fixDep(
pkg: PackageDataEntry,
pkgManifest: Manifest, pkgManifest: Manifest,
action: 'install' | 'update' | 'configure', action: 'install' | 'update' | 'configure',
id: string, id: string,
@@ -167,22 +170,21 @@ export class AppShowPage {
switch (action) { switch (action) {
case 'install': case 'install':
case 'update': case 'update':
return this.installDep(pkgManifest, id) return this.installDep(pkg, pkgManifest, id)
case 'configure': case 'configure':
return this.configureDep(pkgManifest, id) return this.configureDep(pkgManifest, id)
} }
} }
private async installDep( private async installDep(
pkg: PackageDataEntry,
pkgManifest: Manifest, pkgManifest: Manifest,
depId: string, depId: string,
): Promise<void> { ): Promise<void> {
const version = pkgManifest.dependencies[depId].version
const dependentInfo: DependentInfo = { const dependentInfo: DependentInfo = {
id: pkgManifest.id, id: pkgManifest.id,
title: pkgManifest.title, title: pkgManifest.title,
version, version: pkg['current-dependencies'][depId].versionRange,
} }
const navigationExtras: NavigationExtras = { const navigationExtras: NavigationExtras = {
state: { dependentInfo }, state: { dependentInfo },

View File

@@ -3,7 +3,7 @@ import { ModalController, ToastController } from '@ionic/angular'
import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared'
import { from } from 'rxjs' import { from } from 'rxjs'
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 } from '@start9labs/marketplace'
@Component({ @Component({
selector: 'app-show-additional', selector: 'app-show-additional',

View File

@@ -1,91 +1,81 @@
<ng-container <ng-container *ngIf="!(healthChecks | empty)">
*ngIf="manifest | toHealthChecks | async | keyvalue: asIsOrder as checks" <ion-item-divider>Health Checks</ion-item-divider>
> <!-- connected -->
<ng-container *ngIf="checks.length"> <ng-container *ngIf="connected$ | async; else disconnected">
<ion-item-divider>Health Checks</ion-item-divider> <ion-item *ngFor="let check of healthChecks | keyvalue">
<!-- connected --> <!-- result -->
<ng-container *ngIf="connected$ | async; else disconnected"> <ng-container *ngIf="check.value.result as result; else noResult">
<ion-item *ngFor="let health of checks"> <ion-spinner
<!-- result --> *ngIf="isLoading(result)"
<ng-container *ngIf="health.value?.result as result; else noResult"> class="icon-spinner"
<ion-spinner color="primary"
*ngIf="isLoading(result)" slot="start"
class="icon-spinner" ></ion-spinner>
color="primary" <ion-icon
slot="start" *ngIf="result === 'success'"
></ion-spinner> slot="start"
<ion-icon name="checkmark"
*ngIf="result === HealthResult.Success" color="success"
slot="start" ></ion-icon>
name="checkmark" <ion-icon
color="success" *ngIf="result === 'failure'"
></ion-icon> slot="start"
<ion-icon name="warning-outline"
*ngIf="result === HealthResult.Failure" color="warning"
slot="start" ></ion-icon>
name="warning-outline" <ion-icon
color="warning" *ngIf="result === 'disabled'"
></ion-icon> slot="start"
<ion-icon name="remove"
*ngIf="result === HealthResult.Disabled" color="dark"
slot="start" ></ion-icon>
name="remove"
color="dark"
></ion-icon>
<ion-label>
<h2 class="bold">
{{ manifest['health-checks'][health.key].name }}
</h2>
<ion-text [color]="result | healthColor">
<p>
<span *ngIf="isReady(result)">{{ result | titlecase }}</span>
<span *ngIf="result === HealthResult.Starting">...</span>
<span *ngIf="result === HealthResult.Failure">
{{ $any(health.value).error }}
</span>
<span *ngIf="result === HealthResult.Loading">
{{ $any(health.value).message }}
</span>
<span
*ngIf="
result === HealthResult.Success &&
manifest['health-checks'][health.key]['success-message']
"
>
:
{{ manifest['health-checks'][health.key]['success-message'] }}
</span>
</p>
</ion-text>
</ion-label>
</ng-container>
<!-- no result -->
<ng-template #noResult>
<ion-spinner
class="icon-spinner"
color="dark"
slot="start"
></ion-spinner>
<ion-label>
<h2 class="bold">
{{ manifest['health-checks'][health.key].name }}
</h2>
<p class="primary">Awaiting result...</p>
</ion-label>
</ng-template>
</ion-item>
</ng-container>
<!-- disconnected -->
<ng-template #disconnected>
<ion-item *ngFor="let health of checks">
<ion-avatar slot="start">
<ion-skeleton-text class="avatar"></ion-skeleton-text>
</ion-avatar>
<ion-label> <ion-label>
<ion-skeleton-text class="label"></ion-skeleton-text> <h2 class="bold">
<ion-skeleton-text class="description"></ion-skeleton-text> {{ check.value.name }}
</h2>
<ion-text [color]="result | healthColor">
<p>
<span *ngIf="isReady(result)">{{ result | titlecase }}</span>
<span *ngIf="result === 'starting'">...</span>
<span
*ngIf="
check.value.result === 'failure' ||
check.value.result === 'loading' ||
check.value.result === 'success'
"
>
{{ check.value.message }}
</span>
</p>
</ion-text>
</ion-label> </ion-label>
</ion-item> </ng-container>
</ng-template> <!-- no result -->
<ng-template #noResult>
<ion-spinner
class="icon-spinner"
color="dark"
slot="start"
></ion-spinner>
<ion-label>
<h2 class="bold">
{{ check.value.name }}
</h2>
<p class="primary">Awaiting result...</p>
</ion-label>
</ng-template>
</ion-item>
</ng-container> </ng-container>
<!-- disconnected -->
<ng-template #disconnected>
<ion-item *ngFor="let health; in: checks">
<ion-avatar slot="start">
<ion-skeleton-text class="avatar"></ion-skeleton-text>
</ion-avatar>
<ion-label>
<ion-skeleton-text class="label"></ion-skeleton-text>
<ion-skeleton-text class="description"></ion-skeleton-text>
</ion-label>
</ion-item>
</ng-template>
</ng-container> </ng-container>

View File

@@ -1,6 +1,11 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { HealthResult, Manifest } from 'src/app/services/patch-db/data-model' import {
HealthCheckResult,
HealthResult,
MainStatus,
} from 'src/app/services/patch-db/data-model'
import { Manifest } from '@start9labs/marketplace'
@Component({ @Component({
selector: 'app-show-health-checks', selector: 'app-show-health-checks',
@@ -10,9 +15,7 @@ import { HealthResult, Manifest } from 'src/app/services/patch-db/data-model'
}) })
export class AppShowHealthChecksComponent { export class AppShowHealthChecksComponent {
@Input() @Input()
manifest!: Manifest healthChecks!: Record<string, HealthCheckResult>
HealthResult = HealthResult
readonly connected$ = this.connectionService.connected$ readonly connected$ = this.connectionService.connected$

View File

@@ -6,10 +6,9 @@ import {
PrimaryStatus, PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service' } from 'src/app/services/pkg-status-rendering.service'
import { import {
Manifest, DataModel,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState,
Status, 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'
@@ -18,7 +17,13 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'
import { hasCurrentDeps } from 'src/app/util/has-deps' import { hasCurrentDeps } from 'src/app/util/has-deps'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { isInstalled, getManifest } from 'src/app/util/get-package-data' import {
isInstalled,
getManifest,
getAllPackages,
} from 'src/app/util/get-package-data'
import { Manifest } from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
@Component({ @Component({
selector: 'app-show-status', selector: 'app-show-status',
@@ -47,6 +52,7 @@ export class AppShowStatusComponent {
private readonly launcherService: UiLauncherService, private readonly launcherService: UiLauncherService,
private readonly modalService: ModalService, private readonly modalService: ModalService,
private readonly connectionService: ConnectionService, private readonly connectionService: ConnectionService,
private readonly patch: PatchDB<DataModel>,
) {} ) {}
get interfaces(): PackageDataEntry['service-interfaces'] { get interfaces(): PackageDataEntry['service-interfaces'] {
@@ -116,7 +122,7 @@ export class AppShowStatusComponent {
const { title, alerts } = this.manifest const { title, alerts } = this.manifest
let message = alerts.stop || '' let message = alerts.stop || ''
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(this.manifest.id, await getAllPackages(this.patch))) {
const depMessage = `Services that depend on ${title} will no longer work properly and may crash` const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
message = message ? `${message}.\n\n${depMessage}` : depMessage message = message ? `${message}.\n\n${depMessage}` : depMessage
} }
@@ -148,7 +154,7 @@ export class AppShowStatusComponent {
} }
async tryRestart(): Promise<void> { async tryRestart(): Promise<void> {
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(this.manifest.id, await getAllPackages(this.patch))) {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Warning', header: 'Warning',
message: `Services that depend on ${this.manifest.title} may temporarily experiences issues`, message: `Services that depend on ${this.manifest.title} may temporarily experiences issues`,

View File

@@ -5,14 +5,13 @@ import { MarkdownComponent } from '@start9labs/shared'
import { import {
DataModel, DataModel,
InstalledState, InstalledState,
Manifest,
PackageDataEntry, PackageDataEntry,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { ModalService } from 'src/app/services/modal.service' import { ModalService } from 'src/app/services/modal.service'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { from, map, Observable } from 'rxjs' import { from, map, Observable } from 'rxjs'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { getManifest } from 'src/app/util/get-package-data' import { Manifest } from '@start9labs/marketplace'
export interface Button { export interface Button {
title: string title: string

View File

@@ -2,13 +2,13 @@ import { Pipe, PipeTransform } from '@angular/core'
import { import {
DataModel, DataModel,
HealthCheckResult, HealthCheckResult,
Manifest,
PackageMainStatus, PackageMainStatus,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { isEmptyObject } from '@start9labs/shared' import { isEmptyObject } from '@start9labs/shared'
import { map, startWith } from 'rxjs/operators' import { map, startWith } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { Manifest } from '@start9labs/marketplace'
@Pipe({ @Pipe({
name: 'toHealthChecks', name: 'toHealthChecks',
@@ -18,26 +18,17 @@ export class ToHealthChecksPipe implements PipeTransform {
transform( transform(
manifest: Manifest, manifest: Manifest,
): Observable<Record<string, HealthCheckResult | null>> | null { ): Observable<Record<string, HealthCheckResult | null> | null> {
const healthChecks = Object.keys(manifest['health-checks']).reduce( return this.patch
(obj, key) => ({ ...obj, [key]: null }),
{},
)
const healthChecks$ = this.patch
.watch$('package-data', manifest.id, 'status', 'main') .watch$('package-data', manifest.id, 'status', 'main')
.pipe( .pipe(
map(main => { map(main => {
// Question: is this ok or do we have to use Object.keys
// to maintain order and the keys initially present in pkg?
return main.status === PackageMainStatus.Running && return main.status === PackageMainStatus.Running &&
!isEmptyObject(main.health) !isEmptyObject(main.health)
? main.health ? main.health
: healthChecks : null
}), }),
startWith(healthChecks), startWith(null),
) )
return isEmptyObject(healthChecks) ? null : healthChecks$
} }
} }

View File

@@ -8,9 +8,14 @@
View Installed View Installed
</ion-button> </ion-button>
<ng-container *ngIf="localPkg; else install"> <ng-container *ngIf="localPkg; else install">
<ng-container *ngIf="localPkg['state-info'].state === 'installed'"> <ng-container
*ngIf="
localPkg['state-info'].state === 'installed' &&
(localPkg | toManifest) as manifest
"
>
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === -1" *ngIf="(manifest.version | compareEmver: pkg.manifest.version) === -1"
expand="block" expand="block"
color="success" color="success"
(click)="tryInstall()" (click)="tryInstall()"
@@ -18,7 +23,7 @@
Update Update
</ion-button> </ion-button>
<ion-button <ion-button
*ngIf="(localVersion | compareEmver: pkg.manifest.version) === 1" *ngIf="(manifest.version | compareEmver: pkg.manifest.version) === 1"
expand="block" expand="block"
color="warning" color="warning"
(click)="tryInstall()" (click)="tryInstall()"
@@ -27,7 +32,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="(manifest.version | compareEmver: pkg.manifest.version) === 0"
expand="block" expand="block"
color="success" color="success"
(click)="tryInstall()" (click)="tryInstall()"

View File

@@ -7,6 +7,7 @@ import {
import { AlertController, LoadingController } from '@ionic/angular' import { AlertController, LoadingController } from '@ionic/angular'
import { import {
AbstractMarketplaceService, AbstractMarketplaceService,
Manifest,
MarketplacePkg, MarketplacePkg,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { import {
@@ -18,7 +19,6 @@ import {
import { import {
DataModel, DataModel,
PackageDataEntry, PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { ClientStorageService } from 'src/app/services/client-storage.service' import { ClientStorageService } from 'src/app/services/client-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service' import { MarketplaceService } from 'src/app/services/marketplace.service'
@@ -57,10 +57,6 @@ export class MarketplaceShowControlsComponent {
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
) {} ) {}
get localVersion(): string {
return this.localPkg ? getManifest(this.localPkg).version : ''
}
async tryInstall() { async tryInstall() {
const currentMarketplace = await firstValueFrom( const currentMarketplace = await firstValueFrom(
this.marketplaceService.getSelectedHost$(), this.marketplaceService.getSelectedHost$(),
@@ -80,10 +76,12 @@ export class MarketplaceShowControlsComponent {
if (!proceed) return if (!proceed) return
} }
const localManifest = getManifest(this.localPkg)
if ( if (
this.emver.compare(this.localVersion, this.pkg.manifest.version) !== this.emver.compare(localManifest.version, this.pkg.manifest.version) !==
0 && 0 &&
hasCurrentDeps(this.localPkg) hasCurrentDeps(localManifest.id, await getAllPackages(this.patch))
) { ) {
this.dryInstall(url) this.dryInstall(url)
} else { } else {

View File

@@ -19,6 +19,7 @@ import { MarketplaceShowPage } from './marketplace-show.page'
import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component' import { MarketplaceShowHeaderComponent } from './marketplace-show-header/marketplace-show-header.component'
import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component' import { MarketplaceShowDependentComponent } from './marketplace-show-dependent/marketplace-show-dependent.component'
import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component' import { MarketplaceShowControlsComponent } from './marketplace-show-controls/marketplace-show-controls.component'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -41,6 +42,7 @@ const routes: Routes = [
AboutModule, AboutModule,
DependenciesModule, DependenciesModule,
AdditionalModule, AdditionalModule,
UiPipeModule,
], ],
declarations: [ declarations: [
MarketplaceShowPage, MarketplaceShowPage,

View File

@@ -1,10 +1,10 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { isPlatform, LoadingController, NavController } from '@ionic/angular' import { isPlatform, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Manifest } from 'src/app/services/patch-db/data-model'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import cbor from 'cbor' import cbor from 'cbor'
import { ErrorToastService } from '@start9labs/shared' import { ErrorToastService } from '@start9labs/shared'
import { Manifest } from '@start9labs/marketplace'
interface Positions { interface Positions {
[key: string]: [bigint, bigint] // [position, length] [key: string]: [bigint, bigint] // [position, length]
@@ -133,9 +133,7 @@ export class SideloadPage {
} }
async getIcon(positions: Positions, file: Blob) { async getIcon(positions: Positions, file: Blob) {
const contentType = `image/${this.toUpload.manifest?.assets.icon const contentType = '' // @TODO
.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]),

View File

@@ -5,7 +5,6 @@ import { RouterModule, Routes } from '@angular/router'
import { FilterUpdatesPipe, UpdatesPage } from './updates.page' import { FilterUpdatesPipe, UpdatesPage } from './updates.page'
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 { import {
EmverDisplayPipe,
EmverPipesModule, EmverPipesModule,
MarkdownPipeModule, MarkdownPipeModule,
SharedPipesModule, SharedPipesModule,
@@ -14,7 +13,6 @@ import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/sk
import { RoundProgressModule } from 'angular-svg-round-progressbar' import { RoundProgressModule } from 'angular-svg-round-progressbar'
import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module' import { InstallingProgressPipeModule } from 'src/app/pipes/install-progress/install-progress.module'
import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module'
import { MimeTypePipeModule } from '@start9labs/marketplace'
const routes: Routes = [ const routes: Routes = [
{ {
@@ -37,7 +35,6 @@ const routes: Routes = [
InstallingProgressPipeModule, InstallingProgressPipeModule,
StoreIconComponentModule, StoreIconComponentModule,
EmverPipesModule, EmverPipesModule,
MimeTypePipeModule,
], ],
}) })
export class UpdatesPageModule {} export class UpdatesPageModule {}

View File

@@ -33,7 +33,7 @@
<ion-accordion *ngIf="data.localPkgs[pkg.manifest.id] as local"> <ion-accordion *ngIf="data.localPkgs[pkg.manifest.id] as local">
<ion-item lines="none" slot="header"> <ion-item lines="none" slot="header">
<ion-avatar slot="start" style="width: 50px; height: 50px"> <ion-avatar slot="start" style="width: 50px; height: 50px">
<img [src]="pkg | mimeType | trustUrl" /> <img [src]="pkg.icon | trustUrl" />
</ion-avatar> </ion-avatar>
<ion-label> <ion-label>
<h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1> <h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1>
@@ -72,7 +72,7 @@
></ion-spinner> ></ion-spinner>
<ng-template #updateBtn> <ng-template #updateBtn>
<ion-button <ion-button
(click)="tryUpdate(pkg.manifest, host.url, local, $event)" (click)="tryUpdate(pkg.manifest, host.url, $event)"
[color]="marketplaceService.updateErrors[pkg.manifest.id] ? 'danger' : 'tertiary'" [color]="marketplaceService.updateErrors[pkg.manifest.id] ? 'danger' : 'tertiary'"
strong strong
> >

View File

@@ -10,7 +10,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'
@@ -70,12 +70,7 @@ export class UpdatesPage {
}) })
} }
async tryUpdate( async tryUpdate(manifest: Manifest, url: string, e: Event): Promise<void> {
manifest: MarketplaceManifest,
url: string,
local: PackageDataEntry,
e: Event,
): Promise<void> {
e.stopPropagation() e.stopPropagation()
const { id, version } = manifest const { id, version } = manifest
@@ -83,14 +78,15 @@ export class UpdatesPage {
delete this.marketplaceService.updateErrors[id] delete this.marketplaceService.updateErrors[id]
this.marketplaceService.updateQueue[id] = true this.marketplaceService.updateQueue[id] = true
if (hasCurrentDeps(local)) { // manifest.id OK because same as local id for update
if (hasCurrentDeps(manifest.id, await getAllPackages(this.patch))) {
this.dryInstall(manifest, url) this.dryInstall(manifest, url)
} else { } else {
this.install(id, version, url) this.install(id, version, url)
} }
} }
private async dryInstall(manifest: MarketplaceManifest, url: string) { private async dryInstall(manifest: Manifest, url: string) {
const { id, version, title } = manifest const { id, version, title } = manifest
const breakages = dryUpdate( const breakages = dryUpdate(

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { Manifest, PackageDataEntry } from '../../services/patch-db/data-model' import { PackageDataEntry } from '../../services/patch-db/data-model'
import { hasUi } from '../../services/config.service' import { hasUi } from '../../services/config.service'
import { getManifest } from 'src/app/util/get-package-data' import { getManifest } from 'src/app/util/get-package-data'
import { Manifest } from '@start9labs/marketplace'
@Pipe({ @Pipe({
name: 'hasUi', name: 'hasUi',

View File

@@ -1,16 +1,17 @@
import { import {
DockerIoFormat,
InstalledState, InstalledState,
Manifest,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
ServerStatusInfo, ServerStatusInfo,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types' import { Metric, NotificationLevel, RR, 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,
Manifest,
MarketplacePkg,
} from '@start9labs/marketplace'
import { Log } from '@start9labs/shared' import { Log } from '@start9labs/shared'
export module Mock { export module Mock {
@@ -57,14 +58,6 @@ export module Mock {
}, },
replaces: ['banks', 'governments'], replaces: ['banks', 'governments'],
'release-notes': 'Taproot, Schnorr, and more.', 'release-notes': 'Taproot, Schnorr, and more.',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
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',
@@ -79,231 +72,9 @@ export module Mock {
start: 'Starting Bitcoin is good for your health.', start: 'Starting Bitcoin is good for your health.',
stop: null, stop: null,
}, },
'health-checks': {}, 'os-version': '0.2.12',
config: {
get: null,
set: null,
},
volumes: {},
'min-os-version': '0.2.12',
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,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
},
name: {
type: 'string',
name: 'Your Name',
description: 'Tell the class your name.',
nullable: true,
masked: false,
copyable: false,
warning: 'You may loose all your money by providing your name.',
},
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
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,
},
'top-speed': {
type: 'number',
name: 'Top Speed',
description: 'The fastest you can possibly run.',
nullable: false,
range: '[-1000, 1000]',
integral: false,
units: 'm/s',
},
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.',
spec: {
name: {
type: 'string',
name: 'Name',
nullable: false,
masked: false,
copyable: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
},
email: {
type: 'string',
name: 'Email',
nullable: false,
masked: false,
copyable: true,
},
},
},
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,
copyable: 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: {
'lan-address': {
name: 'LAN Address',
type: 'pointer',
subtype: 'package',
target: 'lan-address',
description: 'the lan address',
interface: 'tor-address',
'package-id': '12341234',
},
'friendly-name': {
name: 'Friendly Name',
type: 'string',
description: 'the lan address',
nullable: true,
masked: false,
copyable: false,
},
},
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,
copyable: true,
},
},
},
},
},
},
},
dependencies: {}, dependencies: {},
'has-config': true,
} }
export const MockManifestLnd: Manifest = { export const MockManifestLnd: Manifest = {
@@ -315,14 +86,6 @@ export module Mock {
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!', 'release-notes': 'Dual funded channels!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
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',
@@ -337,82 +100,19 @@ export module Mock {
start: 'Starting LND is good for your health.', start: 'Starting LND is good for your health.',
stop: null, stop: null,
}, },
'health-checks': {}, 'os-version': '0.2.12',
config: {
get: null,
set: null,
},
volumes: {},
'min-os-version': '0.2.12',
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',
description: 'LND needs bitcoin to live.', description: 'LND needs bitcoin to live.',
requirement: { optional: true,
type: 'opt-out',
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',
description: description:
'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.',
requirement: { optional: true,
type: 'opt-in',
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
},
config: null,
}, },
}, },
'has-config': true,
} }
export const MockManifestBitcoinProxy: Manifest = { export const MockManifestBitcoinProxy: Manifest = {
@@ -425,14 +125,6 @@ export module Mock {
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!', 'release-notes': 'Even better support for Bitcoin and wallets!',
assets: {
icon: 'icon.png',
license: 'LICENSE.md',
instructions: 'INSTRUCTIONS.md',
docker_images: 'image.tar',
assets: './assets',
scripts: './scripts',
},
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',
@@ -446,84 +138,27 @@ export module Mock {
start: null, start: null,
stop: null, stop: null,
}, },
'health-checks': {}, 'os-version': '0.2.12',
config: { get: {} as any, set: {} as any },
volumes: {},
'min-os-version': '0.2.12',
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',
description: 'Bitcoin Proxy requires a Bitcoin node.', description: 'Bitcoin Proxy requires a Bitcoin node.',
requirement: { optional: false,
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,
},
},
}, },
}, },
'has-config': false,
} }
export const BitcoinDep: DependencyMetadata = { export const BitcoinDep: DependencyMetadata = {
title: 'Bitcoin Core', title: 'Bitcoin Core',
icon: BTC_ICON, icon: BTC_ICON,
optional: false,
hidden: true, hidden: true,
} }
export const ProxyDep: DependencyMetadata = { export const ProxyDep: DependencyMetadata = {
title: 'Bitcoin Proxy', title: 'Bitcoin Proxy',
icon: PROXY_ICON, icon: PROXY_ICON,
optional: true,
hidden: false, hidden: false,
} }
@@ -1765,6 +1400,7 @@ export module Mock {
}, },
'dependency-config-errors': {}, 'dependency-config-errors': {},
}, },
actions: {}, // @TODO need mocks
'service-interfaces': { 'service-interfaces': {
ui: { ui: {
id: 'ui', id: 'ui',
@@ -1981,12 +1617,6 @@ export module Mock {
}, },
}, },
}, },
'current-dependents': {
lnd: {
pointers: [],
'health-checks': [],
},
},
'current-dependencies': {}, 'current-dependencies': {},
'dependency-info': {}, 'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
@@ -2007,6 +1637,7 @@ export module Mock {
}, },
'dependency-config-errors': {}, 'dependency-config-errors': {},
}, },
actions: {},
'service-interfaces': { 'service-interfaces': {
ui: { ui: {
id: 'ui', id: 'ui',
@@ -2115,15 +1746,9 @@ export module Mock {
}, },
}, },
}, },
'current-dependents': {
lnd: {
pointers: [],
'health-checks': [],
},
},
'current-dependencies': { 'current-dependencies': {
bitcoind: { bitcoind: {
pointers: [], versionRange: '>=26.0.0',
'health-checks': [], 'health-checks': [],
}, },
}, },
@@ -2153,6 +1778,7 @@ export module Mock {
'btc-rpc-proxy': 'Username not found', 'btc-rpc-proxy': 'Username not found',
}, },
}, },
actions: {},
'service-interfaces': { 'service-interfaces': {
grpc: { grpc: {
id: 'grpc', id: 'grpc',
@@ -2365,14 +1991,13 @@ export module Mock {
}, },
}, },
}, },
'current-dependents': {},
'current-dependencies': { 'current-dependencies': {
bitcoind: { bitcoind: {
pointers: [], versionRange: '>=26.0.0',
'health-checks': [], 'health-checks': [],
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
pointers: [], versionRange: '>2.0.0', // @TODO
'health-checks': [], 'health-checks': [],
}, },
}, },

View File

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

View File

@@ -571,7 +571,7 @@ export class MockApiService extends ApiService {
setTimeout(async () => { setTimeout(async () => {
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
const id = ids[i] const id = ids[i]
const appPath = `/package-data/${id}/installed/status/main/status` const appPath = `/package-data/${id}/status/main/status`
const appPatch = [ const appPatch = [
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
@@ -735,7 +735,7 @@ export class MockApiService extends ApiService {
const patch = [ const patch = [
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: `/package-data/${params.id}/installed/status/configured`, path: `/package-data/${params.id}/status/configured`,
value: true, value: true,
}, },
] ]
@@ -782,7 +782,7 @@ export class MockApiService extends ApiService {
} }
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> { async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
const path = `/package-data/${params.id}/installed/status/main` const path = `/package-data/${params.id}/status/main`
await pauseFor(2000) await pauseFor(2000)
@@ -865,7 +865,7 @@ export class MockApiService extends ApiService {
): Promise<RR.RestartPackageRes> { ): Promise<RR.RestartPackageRes> {
// first enact stop // first enact stop
await pauseFor(2000) await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main` const path = `/package-data/${params.id}/status/main`
setTimeout(async () => { setTimeout(async () => {
const patch2: Operation<any>[] = [ const patch2: Operation<any>[] = [
@@ -941,7 +941,7 @@ export class MockApiService extends ApiService {
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> { async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
await pauseFor(2000) await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main` const path = `/package-data/${params.id}/status/main`
setTimeout(() => { setTimeout(() => {
const patch2 = [ const patch2 = [

View File

@@ -1,6 +1,5 @@
import { import {
DataModel, DataModel,
DockerIoFormat,
HealthResult, HealthResult,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
@@ -93,26 +92,33 @@ export const mockPatchData: DataModel = {
started: '2021-06-14T20:49:17.774Z', started: '2021-06-14T20:49:17.774Z',
health: { health: {
'ephemeral-health-check': { 'ephemeral-health-check': {
name: 'Ephemeral Health Check',
result: HealthResult.Starting, result: HealthResult.Starting,
}, },
'chain-state': { 'chain-state': {
name: 'Chain State',
result: HealthResult.Loading, result: HealthResult.Loading,
message: 'Bitcoin is syncing from genesis', message: 'Bitcoin is syncing from genesis',
}, },
'p2p-interface': { 'p2p-interface': {
name: 'P2P',
result: HealthResult.Success, result: HealthResult.Success,
message: 'Health check successful',
}, },
'rpc-interface': { 'rpc-interface': {
name: 'RPC',
result: HealthResult.Failure, result: HealthResult.Failure,
error: 'RPC interface unreachable.', message: 'RPC interface unreachable.',
}, },
'unnecessary-health-check': { 'unnecessary-health-check': {
name: 'Unnecessary Health Check',
result: HealthResult.Disabled, result: HealthResult.Disabled,
}, },
}, },
}, },
'dependency-config-errors': {}, 'dependency-config-errors': {},
}, },
actions: {}, // @TODO
'service-interfaces': { 'service-interfaces': {
ui: { ui: {
id: 'ui', id: 'ui',
@@ -329,12 +335,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
}, },
'current-dependents': {
lnd: {
pointers: [],
'health-checks': [],
},
},
'current-dependencies': {}, 'current-dependencies': {},
'dependency-info': {}, 'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/', 'marketplace-url': 'https://registry.start9.com/',
@@ -359,6 +359,7 @@ export const mockPatchData: DataModel = {
'btc-rpc-proxy': 'This is a config unsatisfied error', 'btc-rpc-proxy': 'This is a config unsatisfied error',
}, },
}, },
actions: {},
'service-interfaces': { 'service-interfaces': {
grpc: { grpc: {
id: 'grpc', id: 'grpc',
@@ -569,14 +570,13 @@ export const mockPatchData: DataModel = {
}, },
}, },
}, },
'current-dependents': {},
'current-dependencies': { 'current-dependencies': {
bitcoind: { bitcoind: {
pointers: [], versionRange: '>=26.0.0',
'health-checks': [], 'health-checks': [],
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
pointers: [], versionRange: '>2.0.0',
'health-checks': [], 'health-checks': [],
}, },
}, },

View File

@@ -85,19 +85,14 @@ export class DepErrorService {
} }
} }
const pkgManifest = pkg['state-info'].manifest const versionRange = pkg['current-dependencies'][depId].versionRange
const depManifest = dep['state-info'].manifest const depManifest = dep['state-info'].manifest
// incorrect version // incorrect version
if ( if (!this.emver.satisfies(depManifest.version, versionRange)) {
!this.emver.satisfies(
depManifest.version,
pkgManifest.dependencies[depId].version,
)
) {
return { return {
type: DependencyErrorType.IncorrectVersion, type: DependencyErrorType.IncorrectVersion,
expected: pkgManifest.dependencies[depId].version, expected: versionRange,
received: depManifest.version, received: depManifest.version,
} }
} }

View File

@@ -1,8 +1,9 @@
import { ConfigSpec } from 'src/app/pkg-config/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'
import { types } from '@start9labs/start-sdk' import { types } from '@start9labs/start-sdk'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import { ActionMetadata } from '@start9labs/start-sdk/cjs/sdk/lib/types'
type ServiceInterfaceWithHostInfo = types.ServiceInterfaceWithHostInfo type ServiceInterfaceWithHostInfo = types.ServiceInterfaceWithHostInfo
export interface DataModel { export interface DataModel {
@@ -111,8 +112,8 @@ export type PackageDataEntry<T extends StateInfo = StateInfo> = {
'state-info': T 'state-info': T
icon: Url icon: Url
status: Status status: Status
actions: Record<string, ActionMetadata>
'last-backup': string | null 'last-backup': string | null
'current-dependents': { [id: string]: CurrentDependencyInfo }
'current-dependencies': { [id: string]: CurrentDependencyInfo } 'current-dependencies': { [id: string]: CurrentDependencyInfo }
'dependency-info': { 'dependency-info': {
[id: string]: { [id: string]: {
@@ -152,125 +153,10 @@ export enum PackageState {
} }
export interface CurrentDependencyInfo { export interface CurrentDependencyInfo {
pointers: any[] versionRange: string
'health-checks': string[] // array of health check IDs 'health-checks': string[] // array of health check IDs
} }
export interface Manifest extends MarketplaceManifest<DependencyConfig | null> {
assets: {
license: string // filename
instructions: string // filename
icon: string // filename
docker_images: string // filename
assets: string // path to assets folder
scripts: string // path to scripts folder
}
'health-checks': Record<
string,
ActionImpl & { name: string; 'success-message': string | null }
>
config: ConfigActions | null
volumes: Record<string, Volume>
'min-os-version': string
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 TorConfig {
'port-mapping': { [port: number]: number }
}
export type LanConfig = {
[port: number]: { ssl: boolean; mapping: number }
}
export interface BackupActions {
create: ActionImpl
restore: ActionImpl
}
export interface Migrations {
from: { [versionRange: string]: ActionImpl }
to: { [versionRange: string]: ActionImpl }
}
export interface Action {
name: string
description: string
warning: string | null
implementation: ActionImpl
'allowed-statuses': (PackageMainStatus.Stopped | PackageMainStatus.Running)[]
'input-spec': ConfigSpec | null
}
export interface Status { export interface Status {
configured: boolean configured: boolean
main: MainStatus main: MainStatus
@@ -302,7 +188,7 @@ export interface MainStatusStarting {
export interface MainStatusRunning { export interface MainStatusRunning {
status: PackageMainStatus.Running status: PackageMainStatus.Running
started: string // UTC date string started: string // UTC date string
health: { [id: string]: HealthCheckResult } health: Record<string, HealthCheckResult>
} }
export interface MainStatusBackingUp { export interface MainStatusBackingUp {
@@ -323,12 +209,13 @@ export enum PackageMainStatus {
Restarting = 'restarting', Restarting = 'restarting',
} }
export type HealthCheckResult = export type HealthCheckResult = { name: string } & (
| HealthCheckResultStarting | HealthCheckResultStarting
| HealthCheckResultLoading | HealthCheckResultLoading
| HealthCheckResultDisabled | HealthCheckResultDisabled
| HealthCheckResultSuccess | HealthCheckResultSuccess
| HealthCheckResultFailure | HealthCheckResultFailure
)
export enum HealthResult { export enum HealthResult {
Starting = 'starting', Starting = 'starting',
@@ -348,6 +235,7 @@ export interface HealthCheckResultDisabled {
export interface HealthCheckResultSuccess { export interface HealthCheckResultSuccess {
result: HealthResult.Success result: HealthResult.Success
message: string
} }
export interface HealthCheckResultLoading { export interface HealthCheckResultLoading {
@@ -357,7 +245,7 @@ export interface HealthCheckResultLoading {
export interface HealthCheckResultFailure { export interface HealthCheckResultFailure {
result: HealthResult.Failure result: HealthResult.Failure
error: string message: string
} }
export type InstallingInfo = { export type InstallingInfo = {

View File

@@ -25,10 +25,7 @@ export function renderPkgStatus(
if (pkg['state-info'].state === PackageState.Installed) { if (pkg['state-info'].state === PackageState.Installed) {
primary = getPrimaryStatus(pkg.status) primary = getPrimaryStatus(pkg.status)
dependency = getDependencyStatus(depErrors) dependency = getDependencyStatus(depErrors)
health = getHealthStatus( health = getHealthStatus(pkg.status)
pkg.status,
!isEmptyObject(pkg['state-info'].manifest['health-checks']),
)
} else { } else {
primary = pkg['state-info'].state as string as PrimaryStatus primary = pkg['state-info'].state as string as PrimaryStatus
} }
@@ -52,29 +49,26 @@ function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
: DependencyStatus.Satisfied : DependencyStatus.Satisfied
} }
function getHealthStatus( function getHealthStatus(status: Status): HealthStatus | null {
status: Status,
hasHealthChecks: boolean,
): HealthStatus | null {
if (status.main.status !== PackageMainStatus.Running || !status.main.health) { if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
return null return null
} }
const values = Object.values(status.main.health) const values = Object.values(status.main.health)
if (values.some(h => h.result === 'failure')) { if (values.some(h => !h.result)) {
return HealthStatus.Failure return HealthStatus.Waiting
} }
if (!values.length && hasHealthChecks) { if (values.some(h => h.result === 'failure')) {
return HealthStatus.Waiting return HealthStatus.Failure
} }
if (values.some(h => h.result === 'loading')) { if (values.some(h => h.result === 'loading')) {
return HealthStatus.Loading return HealthStatus.Loading
} }
if (values.some(h => !h.result || h.result === 'starting')) { if (values.some(h => h.result === 'starting')) {
return HealthStatus.Starting return HealthStatus.Starting
} }

View File

@@ -13,7 +13,7 @@ export function dryUpdate(
Object.keys(pkg['current-dependencies'] || {}).some( Object.keys(pkg['current-dependencies'] || {}).some(
pkgId => pkgId === id, pkgId => pkgId === id,
) && ) &&
!emver.satisfies(version, getManifest(pkg).dependencies[id].version), !emver.satisfies(version, pkg['current-dependencies'][id].versionRange),
) )
.map(pkg => getManifest(pkg).title) .map(pkg => getManifest(pkg).title)
} }

View File

@@ -3,12 +3,12 @@ import {
DataModel, DataModel,
InstalledState, InstalledState,
InstallingState, InstallingState,
Manifest,
PackageDataEntry, PackageDataEntry,
PackageState, PackageState,
UpdatingState, UpdatingState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { firstValueFrom } from 'rxjs' import { firstValueFrom } from 'rxjs'
import { Manifest } from '@start9labs/marketplace'
export async function getPackage( export async function getPackage(
patch: PatchDB<DataModel>, patch: PatchDB<DataModel>,

View File

@@ -1,8 +1,8 @@
import { PackageDataEntry } from '../services/patch-db/data-model' import { PackageDataEntry } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function hasCurrentDeps(pkg: PackageDataEntry): boolean { export function hasCurrentDeps(
return !!Object.keys(pkg['current-dependents']).filter( id: string,
depId => depId !== getManifest(pkg).id, pkgs: Record<string, PackageDataEntry>,
).length ): boolean {
return !!Object.values(pkgs).some(pkg => !!pkg['current-dependencies'][id])
} }