Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major

This commit is contained in:
Matt Hill
2024-11-25 19:02:07 -07:00
712 changed files with 83068 additions and 9240 deletions

View File

@@ -0,0 +1,141 @@
import { Injectable } from '@angular/core'
import { AlertController } from '@ionic/angular'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import {
ActionInputModal,
PackageActionData,
} from '../modals/action-input.component'
const allowedStatuses = {
'only-running': new Set(['running']),
'only-stopped': new Set(['stopped']),
any: new Set([
'running',
'stopped',
'restarting',
'restoring',
'stopping',
'starting',
'backingUp',
]),
}
@Injectable({
providedIn: 'root',
})
export class ActionService {
constructor(
private readonly api: ApiService,
private readonly dialogs: TuiDialogService,
private readonly alertCtrl: AlertController,
private readonly errorService: ErrorService,
private readonly loader: LoadingService,
private readonly formDialog: FormDialogService,
) {}
async present(data: PackageActionData) {
const { pkgInfo, actionInfo } = data
if (
allowedStatuses[actionInfo.metadata.allowedStatuses].has(
pkgInfo.mainStatus,
)
) {
if (actionInfo.metadata.hasInput) {
this.formDialog.open<PackageActionData>(ActionInputModal, {
label: actionInfo.metadata.name,
data,
})
} else {
if (actionInfo.metadata.warning) {
const alert = await this.alertCtrl.create({
header: 'Warning',
message: actionInfo.metadata.warning,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Run',
handler: () => {
this.execute(pkgInfo.id, actionInfo.id)
},
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',
})
await alert.present()
} else {
this.execute(pkgInfo.id, actionInfo.id)
}
}
} else {
const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]]
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 "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`,
buttons: ['OK'],
cssClass: 'alert-error-message enter-click',
})
await alert.present()
}
}
async execute(
packageId: string,
actionId: string,
input?: object,
): Promise<boolean> {
const loader = this.loader.open('Loading...').subscribe()
try {
const res = await this.api.runAction({
packageId,
actionId,
input: input || null,
})
if (!res) return true
if (res.result) {
this.dialogs
.open(new PolymorpheusComponent(ActionSuccessPage), {
label: res.title,
data: res,
})
.subscribe()
} else if (res.message) {
this.dialogs.open(res.message, { label: res.title }).subscribe()
}
return true // needed to dismiss original modal/alert
} catch (e: any) {
this.errorService.handleError(e)
return false // don't dismiss original modal/alert
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -3,18 +3,13 @@ import {
PackageDataEntry,
ServerStatusInfo,
} from 'src/app/services/patch-db/data-model'
import { RR, ServerMetrics, ServerNotifications } from './api.types'
import { RR, ServerNotifications } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
import { Log } from '@start9labs/shared'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { T, CB } from '@start9labs/start-sdk'
import { T, ISB, IST } from '@start9labs/start-sdk'
import { GetPackagesRes } from '@start9labs/marketplace'
const mockBlake3Commitment: T.Blake3Commitment = {
hash: 'fakehash',
size: 0,
}
const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = {
rootSighash: 'fakehash',
rootMaxsize: 0,
@@ -113,7 +108,6 @@ export module Mock {
},
osVersion: '0.2.12',
dependencies: {},
hasConfig: true,
images: {
main: {
source: 'packed',
@@ -124,7 +118,7 @@ export module Mock {
assets: [],
volumes: ['main'],
hardwareRequirements: {
device: {},
device: [],
arch: null,
ram: null,
},
@@ -171,7 +165,6 @@ export module Mock {
s9pk: '',
},
},
hasConfig: true,
images: {
main: {
source: 'packed',
@@ -182,7 +175,7 @@ export module Mock {
assets: [],
volumes: ['main'],
hardwareRequirements: {
device: {},
device: [],
arch: null,
ram: null,
},
@@ -222,7 +215,6 @@ export module Mock {
s9pk: '',
},
},
hasConfig: false,
images: {
main: {
source: 'packed',
@@ -233,7 +225,7 @@ export module Mock {
assets: [],
volumes: ['main'],
hardwareRequirements: {
device: {},
device: [],
arch: null,
ram: null,
},
@@ -262,7 +254,7 @@ export module Mock {
'26.1.0:0.1.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -295,7 +287,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -338,7 +330,7 @@ export module Mock {
'26.1.0:0.1.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -371,7 +363,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -416,7 +408,7 @@ export module Mock {
'0.17.5:0': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -472,7 +464,7 @@ export module Mock {
'0.17.4-beta:1.0-alpha': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -530,7 +522,7 @@ export module Mock {
'0.3.2.6:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -581,7 +573,7 @@ export module Mock {
'27.0.0:1.0.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -614,7 +606,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -657,7 +649,7 @@ export module Mock {
'0.18.0:0.0.1': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -713,7 +705,7 @@ export module Mock {
'0.3.2.7:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -957,14 +949,6 @@ export module Mock {
},
}
export const ActionResponse: RR.ExecutePackageActionRes = {
message:
'Password changed successfully. If you lose your new password, you will be lost forever.',
value: 'NewPassword1234!',
copyable: true,
qr: true,
}
export const SshKeys: RR.GetSSHKeysRes = [
{
createdAt: new Date().toISOString(),
@@ -1160,45 +1144,128 @@ export module Mock {
},
}
export const getInputSpec = async (): Promise<
RR.GetPackageConfigRes['spec']
> =>
export const ActionResMessage: RR.ActionRes = {
version: '1',
title: 'New Password',
message:
'Action was run successfully and smoothly and fully and all is good on the western front.',
result: null,
}
export const ActionResSingle: RR.ActionRes = {
version: '1',
title: 'New Password',
message:
'Action was run successfully and smoothly and fully and all is good on the western front.',
result: {
type: 'single',
copyable: true,
qr: true,
masked: true,
value: 'iwejdoiewdhbew',
},
}
export const ActionResGroup: RR.ActionRes = {
version: '1',
title: 'Properties',
message:
'Successfully retrieved properties. Here is a bunch of useful information about this service.',
result: {
type: 'group',
value: [
{
type: 'single',
name: 'LND Connect',
description: 'This is some information about the thing.',
copyable: true,
qr: true,
masked: true,
value:
'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA',
},
{
type: 'group',
name: 'Nested Stuff',
description: 'This is a nested thing metric',
value: [
{
type: 'single',
name: 'Last Name',
description: 'The last name of the user',
copyable: true,
qr: true,
masked: false,
value: 'Hill',
},
{
type: 'single',
name: 'Age',
description: 'The age of the user',
copyable: false,
qr: false,
masked: false,
value: '35',
},
{
type: 'single',
name: 'Password',
description: 'A secret password',
copyable: true,
qr: false,
masked: true,
value: 'password123',
},
],
},
{
type: 'single',
name: 'Another Value',
description: 'Some more information about the service.',
copyable: false,
qr: true,
masked: false,
value: 'https://guessagain.com',
},
],
},
}
export const getActionInputSpec = async (): Promise<IST.InputSpec> =>
configBuilderToSpec(
CB.Config.of({
bitcoin: CB.Value.object(
ISB.InputSpec.of({
bitcoin: ISB.Value.object(
{
name: 'Bitcoin Settings',
description:
'RPC and P2P interface configuration options for Bitcoin Core',
},
CB.Config.of({
'bitcoind-p2p': CB.Value.union(
ISB.InputSpec.of({
'bitcoind-p2p': ISB.Value.union(
{
name: 'P2P Settings',
description:
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
required: { default: 'internal' },
default: 'internal',
},
CB.Variants.of({
internal: { name: 'Bitcoin Core', spec: CB.Config.of({}) },
ISB.Variants.of({
internal: { name: 'Bitcoin Core', spec: ISB.InputSpec.of({}) },
external: {
name: 'External Node',
spec: CB.Config.of({
'p2p-host': CB.Value.text({
spec: ISB.InputSpec.of({
'p2p-host': ISB.Value.text({
name: 'Public Address',
required: {
default: null,
},
required: false,
default: null,
description:
'The public address of your Bitcoin Core server',
}),
'p2p-port': CB.Value.number({
'p2p-port': ISB.Value.number({
name: 'P2P Port',
description:
'The port that your Bitcoin Core P2P server is bound to',
required: {
default: 8333,
},
required: true,
default: 8333,
min: 0,
max: 65535,
integer: true,
@@ -1209,24 +1276,25 @@ export module Mock {
),
}),
),
color: CB.Value.color({
color: ISB.Value.color({
name: 'Color',
required: false,
default: null,
}),
datetime: CB.Value.datetime({
datetime: ISB.Value.datetime({
name: 'Datetime',
required: false,
default: null,
}),
file: CB.Value.file({
name: 'File',
required: false,
extensions: ['png', 'pdf'],
}),
users: CB.Value.multiselect({
// file: ISB.Value.file({
// name: 'File',
// required: false,
// extensions: ['png', 'pdf'],
// }),
users: ISB.Value.multiselect({
name: 'Users',
default: [],
maxLength: 2,
disabled: ['matt'],
values: {
matt: 'Matt Hill',
alex: 'Alex Inkin',
@@ -1234,25 +1302,22 @@ export module Mock {
lucy: 'Lucy',
},
}),
advanced: CB.Value.object(
advanced: ISB.Value.object(
{
name: 'Advanced',
description: 'Advanced settings',
},
CB.Config.of({
rpcsettings: CB.Value.object(
ISB.InputSpec.of({
rpcsettings: ISB.Value.object(
{
name: 'RPC Settings',
description: 'rpc username and password',
warning:
'Adding RPC users gives them special permissions on your node.',
},
CB.Config.of({
rpcuser2: CB.Value.text({
ISB.InputSpec.of({
rpcuser2: ISB.Value.text({
name: 'RPC Username',
required: {
default: 'defaultrpcusername',
},
required: false,
default: 'defaultrpcusername',
description: 'rpc username',
patterns: [
{
@@ -1261,11 +1326,10 @@ export module Mock {
},
],
}),
rpcuser: CB.Value.text({
rpcuser: ISB.Value.text({
name: 'RPC Username',
required: {
default: 'defaultrpcusername',
},
required: true,
default: 'defaultrpcusername',
description: 'rpc username',
patterns: [
{
@@ -1274,23 +1338,21 @@ export module Mock {
},
],
}),
rpcpass: CB.Value.text({
rpcpass: ISB.Value.text({
name: 'RPC User Password',
required: {
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
required: true,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
description: 'rpc password',
}),
rpcpass2: CB.Value.text({
rpcpass2: ISB.Value.text({
name: 'RPC User Password',
required: {
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
required: true,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
description: 'rpc password',
}),
@@ -1298,15 +1360,15 @@ export module Mock {
),
}),
),
testnet: CB.Value.toggle({
testnet: ISB.Value.toggle({
name: 'Testnet',
default: true,
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!',
}),
'object-list': CB.Value.list(
CB.List.obj(
'object-list': ISB.Value.list(
ISB.List.obj(
{
name: 'Object List',
minLength: 0,
@@ -1318,19 +1380,19 @@ export module Mock {
description: 'This is a list of objects, like users or something',
},
{
spec: CB.Config.of({
'first-name': CB.Value.text({
spec: ISB.InputSpec.of({
'first-name': ISB.Value.text({
name: 'First Name',
required: false,
description: 'User first name',
default: 'Matt',
}),
'last-name': CB.Value.text({
'last-name': ISB.Value.text({
name: 'Last Name',
required: {
default: {
charset: 'a-g,2-9',
len: 12,
},
required: true,
default: {
charset: 'a-g,2-9',
len: 12,
},
description: 'User first name',
patterns: [
@@ -1340,11 +1402,12 @@ export module Mock {
},
],
}),
age: CB.Value.number({
age: ISB.Value.number({
name: 'Age',
description: 'The age of the user',
warning: 'User must be at least 18.',
required: false,
default: null,
min: 18,
integer: false,
}),
@@ -1354,8 +1417,8 @@ export module Mock {
},
),
),
'union-list': CB.Value.list(
CB.List.obj(
'union-list': ISB.Value.list(
ISB.List.obj(
{
name: 'Union List',
minLength: 0,
@@ -1365,32 +1428,29 @@ export module Mock {
warning: 'If you change this, things may work.',
},
{
spec: CB.Config.of({
spec: ISB.InputSpec.of({
/* TODO: Convert range for this value ([0, 2])*/
union: CB.Value.union(
union: ISB.Value.union(
{
name: 'Preference',
description: null,
warning: null,
required: { default: 'summer' },
default: 'summer',
},
CB.Variants.of({
ISB.Variants.of({
summer: {
name: 'summer',
spec: CB.Config.of({
'favorite-tree': CB.Value.text({
spec: ISB.InputSpec.of({
'favorite-tree': ISB.Value.text({
name: 'Favorite Tree',
required: {
default: 'Maple',
},
required: true,
default: 'Maple',
description: 'What is your favorite tree?',
}),
'favorite-flower': CB.Value.select({
'favorite-flower': ISB.Value.select({
name: 'Favorite Flower',
description: 'Select your favorite flower',
required: {
default: 'none',
},
default: 'none',
values: {
none: 'none',
red: 'red',
@@ -1402,8 +1462,8 @@ export module Mock {
},
winter: {
name: 'winter',
spec: CB.Config.of({
'like-snow': CB.Value.toggle({
spec: ISB.InputSpec.of({
'like-snow': ISB.Value.toggle({
name: 'Like Snow?',
default: true,
description: 'Do you like snow or not?',
@@ -1417,60 +1477,59 @@ export module Mock {
},
),
),
'random-select': CB.Value.select({
'random-select': ISB.Value.dynamicSelect(() => ({
name: 'Random select',
description: 'This is not even real.',
warning: 'Be careful changing this!',
required: {
default: null,
},
default: 'option1',
values: {
option1: 'option1',
option2: 'option2',
option3: 'option3',
},
disabled: ['option2'],
}),
})),
'favorite-number':
/* TODO: Convert range for this value ((-100,100])*/ CB.Value.number({
name: 'Favorite Number',
description: 'Your favorite number of all time',
warning:
'Once you set this number, it can never be changed without severe consequences.',
required: {
/* TODO: Convert range for this value ((-100,100])*/ ISB.Value.number(
{
name: 'Favorite Number',
description: 'Your favorite number of all time',
warning:
'Once you set this number, it can never be changed without severe consequences.',
required: false,
default: 7,
integer: false,
units: 'BTC',
},
integer: false,
units: 'BTC',
}),
rpcsettings: CB.Value.object(
),
rpcsettings: ISB.Value.object(
{
name: 'RPC Settings',
description: 'rpc username and password',
warning:
'Adding RPC users gives them special permissions on your node.',
},
CB.Config.of({
laws: CB.Value.object(
ISB.InputSpec.of({
laws: ISB.Value.object(
{
name: 'Laws',
description: 'the law of the realm',
},
CB.Config.of({
law1: CB.Value.text({
ISB.InputSpec.of({
law1: ISB.Value.text({
name: 'First Law',
required: false,
description: 'the first law',
default: null,
}),
law2: CB.Value.text({
law2: ISB.Value.text({
name: 'Second Law',
required: false,
description: 'the second law',
default: null,
}),
}),
),
rulemakers: CB.Value.list(
CB.List.obj(
rulemakers: ISB.Value.list(
ISB.List.obj(
{
name: 'Rule Makers',
minLength: 0,
@@ -1478,22 +1537,20 @@ export module Mock {
description: 'the people who make the rules',
},
{
spec: CB.Config.of({
rulemakername: CB.Value.text({
spec: ISB.InputSpec.of({
rulemakername: ISB.Value.text({
name: 'Rulemaker Name',
required: {
default: {
charset: 'a-g,2-9',
len: 12,
},
required: true,
default: {
charset: 'a-g,2-9',
len: 12,
},
description: 'the name of the rule maker',
}),
rulemakerip: CB.Value.text({
rulemakerip: ISB.Value.text({
name: 'Rulemaker IP',
required: {
default: '192.168.1.0',
},
required: true,
default: '192.168.1.0',
description: 'the ip of the rule maker',
patterns: [
{
@@ -1507,11 +1564,10 @@ export module Mock {
},
),
),
rpcuser: CB.Value.text({
rpcuser: ISB.Value.text({
name: 'RPC Username',
required: {
default: 'defaultrpcusername',
},
required: true,
default: 'defaultrpcusername',
description: 'rpc username',
patterns: [
{
@@ -1520,50 +1576,47 @@ export module Mock {
},
],
}),
rpcpass: CB.Value.text({
rpcpass: ISB.Value.text({
name: 'RPC User Password',
required: {
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
required: true,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
description: 'rpc password',
masked: true,
}),
}),
),
'bitcoin-node': CB.Value.union(
'bitcoin-node': ISB.Value.union(
{
name: 'Bitcoin Node',
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
warning: 'Careful changing this',
required: { default: 'internal' },
disabled: ['fake'],
default: 'internal',
},
CB.Variants.of({
ISB.Variants.of({
fake: {
name: 'Fake',
spec: CB.Config.of({}),
spec: ISB.InputSpec.of({}),
},
internal: {
name: 'Internal',
spec: CB.Config.of({}),
spec: ISB.InputSpec.of({}),
},
external: {
name: 'External',
spec: CB.Config.of({
'emergency-contact': CB.Value.object(
spec: ISB.InputSpec.of({
'emergency-contact': ISB.Value.object(
{
name: 'Emergency Contact',
description: 'The person to contact in case of emergency.',
},
CB.Config.of({
name: CB.Value.text({
ISB.InputSpec.of({
name: ISB.Value.text({
name: 'Name',
required: {
default: null,
},
required: false,
default: null,
patterns: [
{
regex: '^[a-zA-Z]+$',
@@ -1571,20 +1624,18 @@ export module Mock {
},
],
}),
email: CB.Value.text({
email: ISB.Value.text({
name: 'Email',
inputmode: 'email',
required: {
default: null,
},
required: false,
default: null,
}),
}),
),
'public-domain': CB.Value.text({
'public-domain': ISB.Value.text({
name: 'Public Domain',
required: {
default: 'bitcoinnode.com',
},
required: true,
default: 'bitcoinnode.com',
description: 'the public address of the node',
patterns: [
{
@@ -1593,11 +1644,10 @@ export module Mock {
},
],
}),
'private-domain': CB.Value.text({
'private-domain': ISB.Value.text({
name: 'Private Domain',
required: {
default: null,
},
required: false,
default: null,
description: 'the private address of the node',
masked: true,
inputmode: 'url',
@@ -1606,31 +1656,31 @@ export module Mock {
},
}),
),
port: CB.Value.number({
port: ISB.Value.number({
name: 'Port',
description:
'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
required: {
default: 8333,
},
required: true,
default: 8333,
min: 1,
max: 9998,
step: 1,
integer: true,
}),
'favorite-slogan': CB.Value.text({
'favorite-slogan': ISB.Value.text({
name: 'Favorite Slogan',
generate: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
required: false,
default: null,
description:
'You most favorite slogan in the whole world, used for paying you.',
masked: true,
}),
rpcallowip: CB.Value.list(
CB.List.text(
rpcallowip: ISB.Value.list(
ISB.List.text(
{
name: 'RPC Allowed IPs',
minLength: 1,
@@ -1652,8 +1702,8 @@ export module Mock {
},
),
),
rpcauth: CB.Value.list(
CB.List.text(
rpcauth: ISB.Value.list(
ISB.List.text(
{
name: 'RPC Auth',
description:
@@ -1719,14 +1769,40 @@ export module Mock {
lastBackup: null,
nextBackup: null,
status: {
configured: true,
main: {
status: 'running',
started: new Date().toISOString(),
health: {},
main: 'running',
started: new Date().toISOString(),
health: {},
},
actions: {
config: {
name: 'Set Config',
description: 'edit bitcoin.conf',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
properties: {
name: 'View Properties',
description: 'view important information about Bitcoin',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: false,
group: null,
},
test: {
name: 'Do Another Thing',
description:
'An example of an action that shows a warning and takes no input',
warning: 'careful running this action',
visibility: { disabled: 'This is temporarily disabled' },
allowedStatuses: 'only-running',
hasInput: false,
group: null,
},
},
actions: {},
serviceInterfaces: {
ui: {
id: 'ui',
@@ -1886,6 +1962,27 @@ export module Mock {
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {
'bitcoind-config': {
request: {
packageId: 'bitcoind',
actionId: 'config',
severity: 'critical',
reason:
'You must run Config before starting Bitcoin for the first time',
},
active: true,
},
'bitcoind-properties': {
request: {
packageId: 'bitcoind',
actionId: 'properties',
severity: 'important',
reason: 'Check out all the info about your Bitcoin node',
},
active: true,
},
},
}
export const bitcoinProxy: PackageDataEntry<InstalledState> = {
@@ -1899,10 +1996,7 @@ export module Mock {
lastBackup: null,
nextBackup: null,
status: {
configured: false,
main: {
status: 'stopped',
},
main: 'stopped',
},
actions: {},
serviceInterfaces: {
@@ -1930,7 +2024,6 @@ export module Mock {
kind: 'running',
versionRange: '>=26.0.0',
healthChecks: [],
configSatisfied: true,
},
},
hosts: {},
@@ -1938,6 +2031,7 @@ export module Mock {
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {},
}
export const lnd: PackageDataEntry<InstalledState> = {
@@ -1951,10 +2045,7 @@ export module Mock {
lastBackup: null,
nextBackup: null,
status: {
configured: true,
main: {
status: 'stopped',
},
main: 'stopped',
},
actions: {},
serviceInterfaces: {
@@ -2017,14 +2108,12 @@ export module Mock {
kind: 'running',
versionRange: '>=26.0.0',
healthChecks: [],
configSatisfied: true,
},
'btc-rpc-proxy': {
title: Mock.MockManifestBitcoinProxy.title,
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
kind: 'exists',
versionRange: '>2.0.0',
configSatisfied: false,
},
},
hosts: {},
@@ -2032,6 +2121,27 @@ export module Mock {
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {
'bitcoind/config': {
active: true,
request: {
packageId: 'bitcoind',
actionId: 'config',
severity: 'critical',
reason: 'LND likes BTC a certain way',
input: {
kind: 'partial',
value: {
color: '#ffffff',
rpcsettings: {
rpcuser: 'lnd',
},
testnet: false,
},
},
},
},
},
}
export const LocalPkgs: { [key: string]: PackageDataEntry<InstalledState> } =

View File

@@ -6,8 +6,8 @@ import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
import { config } from '@start9labs/start-sdk'
import { Dump } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo } from '@start9labs/shared'
import { CT, T } from '@start9labs/start-sdk'
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
import { IST, T } from '@start9labs/start-sdk'
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
export module RR {
@@ -77,11 +77,10 @@ export module RR {
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & server.tor-logs
export type GetServerLogsRes = FetchLogsRes
// @param limit: BE default is 50
// @param boot: number is offset (0: current, -1 prev, +1 first), string is a specific boot id, and null is all
export type FollowServerLogsReq = {
limit?: number
boot?: number | string | null
limit?: number // (optional) default is 50. Ignored if cursor provided
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
cursor?: string // the last known log. Websocket will return all logs since this log
} // server.logs.follow & server.kernel-logs.follow
export type FollowServerLogsRes = {
startCursor: string
@@ -330,32 +329,27 @@ export module RR {
// package
export type GetPackagePropertiesReq = { id: string } // package.properties
export type GetPackagePropertiesRes = Record<string, string>
export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs
export type GetPackageLogsRes = FetchLogsRes
export type GetPackageLogsReq = ServerLogsReq & { id: string } // package.logs
export type GetPackageLogsRes = LogsRes
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
export type FollowPackageLogsRes = FollowServerLogsRes
export type FollowPackageMetricsReq = { id: string } // package.metrics.follow
export type FollowPackageMetricsRes = {
guid: string
metrics: AppMetrics
}
export type InstallPackageReq = T.InstallParams
export type InstallPackageRes = null
export type GetPackageConfigReq = { id: string } // package.config.get
export type GetPackageConfigRes = { spec: CT.InputSpec; config: object }
export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input
export type GetActionInputRes = {
spec: IST.InputSpec
value: object | null
}
export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry
export type DrySetPackageConfigRes = T.PackageId[]
export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set
export type SetPackageConfigRes = null
export type ActionReq = {
packageId: string
actionId: string
input: object | null
} // package.action.run
export type ActionRes = (T.ActionResult & { version: '1' }) | null
export type RestorePackagesReq = {
// package.backup.restore
@@ -366,13 +360,6 @@ export module RR {
}
export type RestorePackagesRes = null
export type ExecutePackageActionReq = {
id: string
actionId: string
input?: object
} // package.action
export type ExecutePackageActionRes = ActionResponse
export type StartPackageReq = { id: string } // package.start
export type StartPackageRes = null
@@ -382,19 +369,12 @@ export module RR {
export type StopPackageReq = { id: string } // package.stop
export type StopPackageRes = null
export type RebuildPackageReq = { id: string } // package.rebuild
export type RebuildPackageRes = null
export type UninstallPackageReq = { id: string } // package.uninstall
export type UninstallPackageRes = null
export type DryConfigureDependencyReq = {
dependencyId: string
dependentId: string
} // package.dependency.configure.dry
export type DryConfigureDependencyRes = {
oldConfig: object
newConfig: object
spec: CT.InputSpec
}
export type SideloadPackageReq = {
manifest: T.Manifest
icon: string // base64
@@ -441,14 +421,7 @@ export type TaggedDependencyError = {
error: DependencyError
}
export type ActionResponse = {
message: string
value: string | null
copyable: boolean
qr: boolean
}
type MetricData = {
interface MetricData {
value: string
unit: string
}
@@ -667,7 +640,7 @@ export type DependencyError =
| DependencyErrorNotInstalled
| DependencyErrorNotRunning
| DependencyErrorIncorrectVersion
| DependencyErrorConfigUnsatisfied
| DependencyErrorActionRequired
| DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
@@ -685,8 +658,8 @@ export type DependencyErrorIncorrectVersion = {
received: string // version
}
export type DependencyErrorConfigUnsatisfied = {
type: 'configUnsatisfied'
export interface DependencyErrorActionRequired {
type: 'actionRequired'
}
export type DependencyErrorHealthChecksFailed = {

View File

@@ -12,7 +12,7 @@ export abstract class ApiService {
// http
// for sideloading packages
abstract uploadPackage(guid: string, body: Blob): Promise<string>
abstract uploadPackage(guid: string, body: Blob): Promise<void>
abstract uploadFile(body: Blob): Promise<string>
@@ -295,10 +295,6 @@ export abstract class ApiService {
// package
abstract getPackageProperties(
params: RR.GetPackagePropertiesReq,
): Promise<RR.GetPackagePropertiesRes>
abstract getPackageLogs(
params: RR.GetPackageLogsReq,
): Promise<RR.GetPackageLogsRes>
@@ -311,26 +307,16 @@ export abstract class ApiService {
params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes>
abstract getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes>
abstract getActionInput(
params: RR.GetActionInputReq,
): Promise<RR.GetActionInputRes>
abstract drySetPackageConfig(
params: RR.DrySetPackageConfigReq,
): Promise<RR.DrySetPackageConfigRes>
abstract setPackageConfig(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes>
abstract runAction(params: RR.ActionReq): Promise<RR.ActionRes>
abstract restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes>
abstract executePackageAction(
params: RR.ExecutePackageActionReq,
): Promise<RR.ExecutePackageActionRes>
abstract startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes>
abstract restartPackage(
@@ -339,14 +325,14 @@ export abstract class ApiService {
abstract stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes>
abstract rebuildPackage(
params: RR.RebuildPackageReq,
): Promise<RR.RebuildPackageRes>
abstract uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes>
abstract dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes>
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
abstract setInterfaceClearnetAddress(

View File

@@ -44,12 +44,11 @@ export class LiveApiService extends ApiService {
// for sideloading packages
async uploadPackage(guid: string, body: Blob): Promise<string> {
return this.httpRequest({
async uploadPackage(guid: string, body: Blob): Promise<void> {
await this.httpRequest({
method: Method.POST,
body,
url: `/rest/rpc/${guid}`,
responseType: 'text',
})
}
@@ -562,12 +561,6 @@ export class LiveApiService extends ApiService {
// package
async getPackageProperties(
params: RR.GetPackagePropertiesReq,
): Promise<RR.GetPackagePropertiesRes> {
return this.rpcRequest({ method: 'package.properties', params })
}
async getPackageLogs(
params: RR.GetPackageLogsReq,
): Promise<RR.GetPackageLogsRes> {
@@ -586,22 +579,14 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.install', params })
}
async getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes> {
return this.rpcRequest({ method: 'package.config.get', params })
async getActionInput(
params: RR.GetActionInputReq,
): Promise<RR.GetActionInputRes> {
return this.rpcRequest({ method: 'package.action.get-input', params })
}
async drySetPackageConfig(
params: RR.DrySetPackageConfigReq,
): Promise<RR.DrySetPackageConfigRes> {
return this.rpcRequest({ method: 'package.config.set.dry', params })
}
async setPackageConfig(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes> {
return this.rpcRequest({ method: 'package.config.set', params })
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
return this.rpcRequest({ method: 'package.action.run', params })
}
async restorePackages(
@@ -610,12 +595,6 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.backup.restore', params })
}
async executePackageAction(
params: RR.ExecutePackageActionReq,
): Promise<RR.ExecutePackageActionRes> {
return this.rpcRequest({ method: 'package.action', params })
}
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
return this.rpcRequest({ method: 'package.start', params })
}
@@ -630,21 +609,18 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.stop', params })
}
async rebuildPackage(
params: RR.RebuildPackageReq,
): Promise<RR.RebuildPackageRes> {
return this.rpcRequest({ method: 'package.rebuild', params })
}
async uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {
return this.rpcRequest({ method: 'package.uninstall', params })
}
async dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes> {
return this.rpcRequest({
method: 'package.dependency.configure.dry',
params,
})
}
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
return this.rpcRequest({
method: 'package.sideload',

View File

@@ -2,10 +2,12 @@ import { Injectable } from '@angular/core'
import { pauseFor, Log, RPCErrorDetails, RPCOptions } from '@start9labs/shared'
import { ApiService } from './embassy-api.service'
import {
AddOperation,
Operation,
PatchOp,
pathFromArray,
RemoveOperation,
ReplaceOperation,
Revision,
} from 'patch-db-client'
import {
@@ -81,10 +83,8 @@ export class MockApiService extends ApiService {
.subscribe()
}
async uploadPackage(guid: string, body: Blob): Promise<string> {
async uploadPackage(guid: string, body: Blob): Promise<void> {
await pauseFor(2000)
// @TODO Aiden confirm this is correct
return 'success'
}
async uploadFile(body: Blob): Promise<string> {
@@ -112,7 +112,7 @@ export class MockApiService extends ApiService {
openWebsocket$<T>(
guid: string,
config: RR.WebsocketConfig<T>,
config: RR.WebsocketConfig<T> = {},
): Observable<T> {
if (guid === 'db-guid') {
return this.mockWsSource$.pipe<any>(
@@ -131,6 +131,11 @@ export class MockApiService extends ApiService {
return from(this.initProgress()).pipe(
startWith(PROGRESS),
) as Observable<T>
} else if (guid === 'sideload-progress-guid') {
config.openObserver?.next(new Event(''))
return from(this.initProgress()).pipe(
startWith(PROGRESS),
) as Observable<T>
} else {
throw new Error('invalid guid type')
}
@@ -909,14 +914,14 @@ export class MockApiService extends ApiService {
async createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
await pauseFor(2000)
const path = '/serverInfo/statusInfo/backupProgress'
const serverPath = '/serverInfo/statusInfo/backupProgress'
const ids = params.packageIds
setTimeout(async () => {
for (let i = 0; i < ids.length; i++) {
const id = ids[i]
const appPath = `/packageData/${id}/status/main/status`
const appPatch = [
const appPath = `/packageData/${id}/status/main/`
const appPatch: ReplaceOperation<T.MainStatus['main']>[] = [
{
op: PatchOp.REPLACE,
path: appPath,
@@ -933,40 +938,43 @@ export class MockApiService extends ApiService {
value: 'stopped',
},
])
this.mockRevision([
const serverPatch: ReplaceOperation<T.BackupProgress['complete']>[] = [
{
op: PatchOp.REPLACE,
path: `${path}/${id}/complete`,
path: `${serverPath}/${id}/complete`,
value: true,
},
])
]
this.mockRevision(serverPatch)
}
await pauseFor(1000)
// set server back to running
const lastPatch = [
// remove backupProgress
const lastPatch: ReplaceOperation<T.ServerStatus['backupProgress']>[] = [
{
op: PatchOp.REPLACE,
path,
path: serverPath,
value: null,
},
]
this.mockRevision(lastPatch)
}, 500)
const originalPatch = [
{
op: PatchOp.REPLACE,
path,
value: ids.reduce((acc, val) => {
return {
...acc,
[val]: { complete: false },
}
}, {}),
},
]
const originalPatch: ReplaceOperation<T.ServerStatus['backupProgress']>[] =
[
{
op: PatchOp.REPLACE,
path: serverPath,
value: ids.reduce((acc, val) => {
return {
...acc,
[val]: { complete: false },
}
}, {}),
},
]
this.mockRevision(originalPatch)
@@ -975,15 +983,6 @@ export class MockApiService extends ApiService {
// package
async getPackageProperties(
params: RR.GetPackagePropertiesReq,
): Promise<RR.GetPackagePropertiesRes> {
await pauseFor(2000)
return {
password: 'specialPassword$',
}
}
async getPackageLogs(
params: RR.GetPackageLogsReq,
): Promise<RR.GetPackageLogsRes> {
@@ -1025,7 +1024,7 @@ export class MockApiService extends ApiService {
this.installProgress(params.id)
}, 1000)
const patch: Operation<
const patch: AddOperation<
PackageDataEntry<InstallingState | UpdatingState>
>[] = [
{
@@ -1055,44 +1054,43 @@ export class MockApiService extends ApiService {
return null
}
async getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes> {
async getActionInput(
params: RR.GetActionInputReq,
): Promise<RR.GetActionInputRes> {
await pauseFor(2000)
return {
config: Mock.MockConfig,
spec: await Mock.getInputSpec(),
value: Mock.MockConfig,
spec: await Mock.getActionInputSpec(),
}
}
async drySetPackageConfig(
params: RR.DrySetPackageConfigReq,
): Promise<RR.DrySetPackageConfigRes> {
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
await pauseFor(2000)
return []
}
async setPackageConfig(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: `/packageData/${params.id}/status/configured`,
value: true,
},
]
this.mockRevision(patch)
return null
if (params.actionId === 'properties') {
// return Mock.ActionResGroup
return Mock.ActionResMessage
// return Mock.ActionResSingle
} else if (params.actionId === 'config') {
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/packageData/${params.packageId}/requestedActions/${params.packageId}-config`,
},
]
this.mockRevision(patch)
return null
} else {
return Mock.ActionResMessage
// return Mock.ActionResSingle
}
}
async restorePackages(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes> {
await pauseFor(2000)
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
const patch: AddOperation<PackageDataEntry>[] = params.ids.map(id => {
setTimeout(async () => {
this.installProgress(id)
}, 2000)
@@ -1118,50 +1116,62 @@ export class MockApiService extends ApiService {
return null
}
async executePackageAction(
params: RR.ExecutePackageActionReq,
): Promise<RR.ExecutePackageActionRes> {
await pauseFor(2000)
return Mock.ActionResponse
}
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
const path = `/packageData/${params.id}/status/main`
const path = `/packageData/${params.id}/status`
await pauseFor(2000)
setTimeout(async () => {
if (params.id !== 'bitcoind') {
const patch2 = [
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {},
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
{
op: PatchOp.REPLACE,
path,
value: {
main: 'running',
started: new Date().toISOString(),
health: {
'ephemeral-health-check': {
name: 'Ephemeral Health Check',
result: 'starting',
message: null,
},
'unnecessary-health-check': {
name: 'Unnecessary Health Check',
result: 'disabled',
message: 'Custom disabled message',
},
'chain-state': {
name: 'Chain State',
result: 'loading',
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
name: 'P2P Interface',
result: 'success',
message: null,
},
'rpc-interface': {
name: 'RPC Interface',
result: 'failure',
message: 'Custom failure message',
},
},
},
]
this.mockRevision(patch2)
}
const patch3 = [
{
op: PatchOp.REPLACE,
path: path + '/status',
value: 'running',
},
{
op: PatchOp.REPLACE,
path: path + '/started',
value: new Date().toISOString(),
},
]
this.mockRevision(patch3)
this.mockRevision(patch2)
}, 2000)
const originalPatch = [
const originalPatch: ReplaceOperation<
T.MainStatus & { main: 'starting' }
>[] = [
{
op: PatchOp.REPLACE,
path: path + '/status',
value: 'starting',
path,
value: {
main: 'starting',
health: {},
},
},
]
@@ -1173,74 +1183,57 @@ export class MockApiService extends ApiService {
async restartPackage(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes> {
// first enact stop
await pauseFor(2000)
const path = `/packageData/${params.id}/status/main`
const path = `/packageData/${params.id}/status`
setTimeout(async () => {
const patch2: Operation<any>[] = [
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
{
op: PatchOp.REPLACE,
path: path + '/status',
value: 'starting',
},
{
op: PatchOp.ADD,
path: path + '/restarting',
value: true,
path,
value: {
main: 'running',
started: new Date().toISOString(),
health: {
'ephemeral-health-check': {
name: 'Ephemeral Health Check',
result: 'starting',
message: null,
},
'unnecessary-health-check': {
name: 'Unnecessary Health Check',
result: 'disabled',
message: 'Custom disabled message',
},
'chain-state': {
name: 'Chain State',
result: 'loading',
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
name: 'P2P Interface',
result: 'success',
message: null,
},
'rpc-interface': {
name: 'RPC Interface',
result: 'failure',
message: 'Custom failure message',
},
},
},
},
]
this.mockRevision(patch2)
await pauseFor(2000)
const patch3: Operation<any>[] = [
{
op: PatchOp.REPLACE,
path: path + '/status',
value: 'running',
},
{
op: PatchOp.REMOVE,
path: path + '/restarting',
},
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {
'ephemeral-health-check': {
result: 'starting',
},
'unnecessary-health-check': {
result: 'disabled',
},
'chain-state': {
result: 'loading',
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
result: 'success',
},
'rpc-interface': {
result: 'failure',
error: 'RPC interface unreachable.',
},
},
} as any,
]
this.mockRevision(patch3)
}, this.revertTime)
const patch = [
const patch: ReplaceOperation<T.MainStatus & { main: 'restarting' }>[] = [
{
op: PatchOp.REPLACE,
path: path + '/status',
value: 'restarting',
},
{
op: PatchOp.REPLACE,
path: path + '/health',
value: {},
path,
value: {
main: 'restarting',
},
},
]
@@ -1251,29 +1244,24 @@ export class MockApiService extends ApiService {
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
await pauseFor(2000)
const path = `/packageData/${params.id}/status/main`
const path = `/packageData/${params.id}/status`
setTimeout(() => {
const patch2 = [
const patch2: ReplaceOperation<T.MainStatus & { main: 'stopped' }>[] = [
{
op: PatchOp.REPLACE,
path: path,
value: {
status: 'stopped',
},
value: { main: 'stopped' },
},
]
this.mockRevision(patch2)
}, this.revertTime)
const patch = [
const patch: ReplaceOperation<T.MainStatus & { main: 'stopping' }>[] = [
{
op: PatchOp.REPLACE,
path: path,
value: {
status: 'stopping',
timeout: '35s',
},
value: { main: 'stopping' },
},
]
@@ -1282,6 +1270,12 @@ export class MockApiService extends ApiService {
return null
}
async rebuildPackage(
params: RR.RebuildPackageReq,
): Promise<RR.RebuildPackageRes> {
return this.restartPackage(params)
}
async uninstallPackage(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {
@@ -1297,7 +1291,7 @@ export class MockApiService extends ApiService {
this.mockRevision(patch2)
}, this.revertTime)
const patch = [
const patch: ReplaceOperation<T.PackageState['state']>[] = [
{
op: PatchOp.REPLACE,
path: `/packageData/${params.id}/stateInfo/state`,
@@ -1310,22 +1304,11 @@ export class MockApiService extends ApiService {
return null
}
async dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes> {
await pauseFor(2000)
return {
oldConfig: Mock.MockConfig,
newConfig: Mock.MockDependencyConfig,
spec: await Mock.getInputSpec(),
}
}
async sideloadPackage(): Promise<RR.SideloadPackageRes> {
await pauseFor(2000)
return {
upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated
upload: 'sideload-upload-guid', // no significance, randomly generated
progress: 'sideload-progress-guid', // no significance, randomly generated
}
}

View File

@@ -154,40 +154,44 @@ export const mockPatchData: DataModel = {
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
nextBackup: new Date(new Date().valueOf() + 100000000).toISOString(),
status: {
configured: true,
main: {
status: 'running',
started: '2021-06-14T20:49:17.774Z',
health: {
'ephemeral-health-check': {
name: 'Ephemeral Health Check',
result: 'starting',
message: null,
},
'chain-state': {
name: 'Chain State',
result: 'loading',
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
name: 'P2P',
result: 'success',
message: 'Health check successful',
},
'rpc-interface': {
name: 'RPC',
result: 'failure',
message: 'RPC interface unreachable.',
},
'unnecessary-health-check': {
name: 'Unnecessary Health Check',
result: 'disabled',
message: null,
},
},
main: 'stopped',
},
// status: {
// main: 'error',
// message: 'Bitcoin is erroring out',
// debug: 'This is a complete stack trace for bitcoin',
// onRebuild: 'start',
// },
actions: {
config: {
name: 'Set Config',
description: 'edit bitcoin.conf',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
properties: {
name: 'View Properties',
description: 'view important information about Bitcoin',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: false,
group: null,
},
test: {
name: 'Do Another Thing',
description:
'An example of an action that shows a warning and takes no input',
warning: 'careful running this action',
visibility: { disabled: 'This is temporarily disabled' },
allowedStatuses: 'only-running',
hasInput: false,
group: null,
},
},
actions: {},
serviceInterfaces: {
ui: {
id: 'ui',
@@ -347,6 +351,27 @@ export const mockPatchData: DataModel = {
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {
'bitcoind-config': {
request: {
packageId: 'bitcoind',
actionId: 'config',
severity: 'critical',
reason:
'You must run Config before starting Bitcoin for the first time',
},
active: true,
},
'bitcoind-properties': {
request: {
packageId: 'bitcoind',
actionId: 'properties',
severity: 'important',
reason: 'Check out all the info about your Bitcoin node',
},
active: true,
},
},
},
lnd: {
stateInfo: {
@@ -362,10 +387,7 @@ export const mockPatchData: DataModel = {
lastBackup: null,
nextBackup: null,
status: {
configured: true,
main: {
status: 'stopped',
},
main: 'stopped',
},
actions: {},
serviceInterfaces: {
@@ -428,7 +450,6 @@ export const mockPatchData: DataModel = {
kind: 'running',
versionRange: '>=26.0.0',
healthChecks: [],
configSatisfied: true,
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
@@ -436,7 +457,6 @@ export const mockPatchData: DataModel = {
kind: 'running',
versionRange: '>2.0.0',
healthChecks: [],
configSatisfied: false,
},
},
hosts: {},
@@ -444,6 +464,27 @@ export const mockPatchData: DataModel = {
registry: 'https://registry.start9.com/',
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {
'bitcoind/config': {
active: true,
request: {
packageId: 'bitcoind',
actionId: 'config',
severity: 'critical',
reason: 'LND likes BTC a certain way',
input: {
kind: 'partial',
value: {
color: '#ffffff',
rpcsettings: {
rpcuser: 'lnd',
},
testnet: false,
},
},
},
},
},
},
},
}

View File

@@ -75,7 +75,7 @@ export class ConfigService {
isLaunchable(
state: T.PackageState['state'],
status: T.MainStatus['status'],
status: T.MainStatus['main'],
): boolean {
return state === 'installed' && status === 'running'
}

View File

@@ -104,14 +104,21 @@ export class DepErrorService {
}
}
// invalid config
if (!currentDep.configSatisfied) {
// action required
if (
Object.values(pkg.requestedActions).some(
a =>
a.active &&
a.request.packageId === depId &&
a.request.severity === 'critical',
)
) {
return {
type: 'configUnsatisfied',
type: 'actionRequired',
}
}
const depStatus = dep.status.main.status
const depStatus = dep.status.main
// not running
if (depStatus !== 'running' && depStatus !== 'starting') {
@@ -123,7 +130,7 @@ export class DepErrorService {
// health check failure
if (depStatus === 'running' && currentDep.kind === 'running') {
for (let id of currentDep.healthChecks) {
const check = dep.status.main.health[id]
const check = dep.status.health[id]
if (check?.result !== 'success') {
return {
type: 'healthChecksFailed',

View File

@@ -5,7 +5,7 @@ import { OSUpdate } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { getServerInfo } from 'src/app/utils/get-server-info'
import { DataModel } from './patch-db/data-model'
import { Exver } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk'
@Injectable({
providedIn: 'root',
@@ -47,14 +47,14 @@ export class EOSService {
constructor(
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly exver: Exver,
) {}
async loadEos(): Promise<void> {
const { version, id } = await getServerInfo(this.patch)
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
const updateAvailable =
this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater'
Version.parse(this.osUpdate.version).compare(Version.parse(version)) ===
'greater'
this.updateAvailable$.next(updateAvailable)
}
}

View File

@@ -7,7 +7,7 @@ import {
ValidatorFn,
Validators,
} from '@angular/forms'
import { CT, utils } from '@start9labs/start-sdk'
import { IST, utils } from '@start9labs/start-sdk'
const Mustache = require('mustache')
@Injectable({
@@ -17,16 +17,16 @@ export class FormService {
constructor(private readonly formBuilder: UntypedFormBuilder) {}
createForm(
spec: CT.InputSpec,
spec: IST.InputSpec,
current: Record<string, any> = {},
): UntypedFormGroup {
return this.getFormGroup(spec, [], current)
}
getUnionSelectSpec(
spec: CT.ValueSpecUnion,
spec: IST.ValueSpecUnion,
selection: string | null,
): CT.ValueSpecSelect {
): IST.ValueSpecSelect {
return {
...spec,
type: 'select',
@@ -38,7 +38,7 @@ export class FormService {
}
getUnionObject(
spec: CT.ValueSpecUnion,
spec: IST.ValueSpecUnion,
selected: string | null,
): UntypedFormGroup {
const group = this.getFormGroup({
@@ -53,16 +53,16 @@ export class FormService {
return group
}
getListItem(spec: CT.ValueSpecList, entry?: any) {
if (CT.isValueSpecListOf(spec, 'text')) {
getListItem(spec: IST.ValueSpecList, entry?: any) {
if (IST.isValueSpecListOf(spec, 'text')) {
return this.formBuilder.control(entry, stringValidators(spec.spec))
} else if (CT.isValueSpecListOf(spec, 'object')) {
} else if (IST.isValueSpecListOf(spec, 'object')) {
return this.getFormGroup(spec.spec.spec, [], entry)
}
}
getFormGroup(
config: CT.InputSpec,
config: IST.InputSpec,
validators: ValidatorFn[] = [],
current?: Record<string, any> | null,
): UntypedFormGroup {
@@ -77,7 +77,7 @@ export class FormService {
}
private getFormEntry(
spec: CT.ValueSpec,
spec: IST.ValueSpec,
currentValue?: any,
): UntypedFormGroup | UntypedFormArray | UntypedFormControl {
let value: any
@@ -140,7 +140,7 @@ export class FormService {
return this.formBuilder.control(value)
case 'select':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value, selectValidators(spec))
return this.formBuilder.control(value)
case 'multiselect':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value, multiselectValidators(spec))
@@ -150,18 +150,18 @@ export class FormService {
}
}
// function getListItemValidators(spec: CT.ValueSpecList) {
// if (CT.isValueSpecListOf(spec, 'text')) {
// function getListItemValidators(spec: IST.ValueSpecList) {
// if (IST.isValueSpecListOf(spec, 'text')) {
// return stringValidators(spec.spec)
// }
// }
function stringValidators(
spec: CT.ValueSpecText | CT.ListValueSpecText,
spec: IST.ValueSpecText | IST.ListValueSpecText,
): ValidatorFn[] {
const validators: ValidatorFn[] = []
if ((spec as CT.ValueSpecText).required) {
if ((spec as IST.ValueSpecText).required) {
validators.push(Validators.required)
}
@@ -174,7 +174,7 @@ function stringValidators(
return validators
}
function textareaValidators(spec: CT.ValueSpecTextarea): ValidatorFn[] {
function textareaValidators(spec: IST.ValueSpecTextarea): ValidatorFn[] {
const validators: ValidatorFn[] = []
if (spec.required) {
@@ -186,7 +186,7 @@ function textareaValidators(spec: CT.ValueSpecTextarea): ValidatorFn[] {
return validators
}
function colorValidators({ required }: CT.ValueSpecColor): ValidatorFn[] {
function colorValidators({ required }: IST.ValueSpecColor): ValidatorFn[] {
const validators: ValidatorFn[] = [Validators.pattern(/^#[0-9a-f]{6}$/i)]
if (required) {
@@ -200,7 +200,7 @@ function datetimeValidators({
required,
min,
max,
}: CT.ValueSpecDatetime): ValidatorFn[] {
}: IST.ValueSpecDatetime): ValidatorFn[] {
const validators: ValidatorFn[] = []
if (required) {
@@ -218,12 +218,12 @@ function datetimeValidators({
return validators
}
function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] {
function numberValidators(spec: IST.ValueSpecNumber): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(isNumber())
if ((spec as CT.ValueSpecNumber).required) {
if ((spec as IST.ValueSpecNumber).required) {
validators.push(Validators.required)
}
@@ -236,23 +236,13 @@ function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] {
return validators
}
function selectValidators(spec: CT.ValueSpecSelect): ValidatorFn[] {
const validators: ValidatorFn[] = []
if (spec.required) {
validators.push(Validators.required)
}
return validators
}
function multiselectValidators(spec: CT.ValueSpecMultiselect): ValidatorFn[] {
function multiselectValidators(spec: IST.ValueSpecMultiselect): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(listInRange(spec.minLength, spec.maxLength))
return validators
}
function listValidators(spec: CT.ValueSpecList): ValidatorFn[] {
function listValidators(spec: IST.ValueSpecList): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(listInRange(spec.minLength, spec.maxLength))
validators.push(listItemIssue())
@@ -367,7 +357,7 @@ export function listItemIssue(): ValidatorFn {
}
}
export function listUnique(spec: CT.ValueSpecList): ValidatorFn {
export function listUnique(spec: IST.ValueSpecList): ValidatorFn {
return control => {
const list = control.value
for (let idx = 0; idx < list.length; idx++) {
@@ -404,7 +394,11 @@ export function listUnique(spec: CT.ValueSpecList): ValidatorFn {
}
}
function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean {
function listItemEquals(
spec: IST.ValueSpecList,
val1: any,
val2: any,
): boolean {
// TODO: fix types
switch (spec.spec.type) {
case 'text':
@@ -417,7 +411,7 @@ function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean {
}
}
function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
function itemEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean {
switch (spec.type) {
case 'text':
case 'textarea':
@@ -429,15 +423,15 @@ function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
// TODO: 'unique-by' does not exist on ValueSpecObject, fix types
return objEquals(
(spec as any)['unique-by'],
spec as CT.ValueSpecObject,
spec as IST.ValueSpecObject,
val1,
val2,
)
case 'union':
// TODO: 'unique-by' does not exist on CT.ValueSpecUnion, fix types
// TODO: 'unique-by' does not exist onIST.ValueSpecUnion, fix types
return unionEquals(
(spec as any)['unique-by'],
spec as CT.ValueSpecUnion,
spec as IST.ValueSpecUnion,
val1,
val2,
)
@@ -457,8 +451,8 @@ function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean {
}
function listObjEquals(
uniqueBy: CT.UniqueBy,
spec: CT.ListValueSpecObject,
uniqueBy: IST.UniqueBy,
spec: IST.ListValueSpecObject,
val1: any,
val2: any,
): boolean {
@@ -485,8 +479,8 @@ function listObjEquals(
}
function objEquals(
uniqueBy: CT.UniqueBy,
spec: CT.ValueSpecObject,
uniqueBy: IST.UniqueBy,
spec: IST.ValueSpecObject,
val1: any,
val2: any,
): boolean {
@@ -514,8 +508,8 @@ function objEquals(
}
function unionEquals(
uniqueBy: CT.UniqueBy,
spec: CT.ValueSpecUnion,
uniqueBy: IST.UniqueBy,
spec: IST.ValueSpecUnion,
val1: any,
val2: any,
): boolean {
@@ -547,8 +541,8 @@ function unionEquals(
}
function uniqueByMessageWrapper(
uniqueBy: CT.UniqueBy,
spec: CT.ListValueSpecObject,
uniqueBy: IST.UniqueBy,
spec: IST.ListValueSpecObject,
) {
let configSpec = spec.spec
@@ -559,8 +553,8 @@ function uniqueByMessageWrapper(
}
function uniqueByMessage(
uniqueBy: CT.UniqueBy,
configSpec: CT.InputSpec,
uniqueBy: IST.UniqueBy,
configSpec: IST.InputSpec,
outermost = true,
): string {
let joinFunc
@@ -569,7 +563,7 @@ function uniqueByMessage(
return ''
} else if (typeof uniqueBy === 'string') {
return configSpec[uniqueBy]
? (configSpec[uniqueBy] as CT.ValueSpecObject).name
? (configSpec[uniqueBy] as IST.ValueSpecObject).name
: uniqueBy
} else if ('any' in uniqueBy) {
joinFunc = ' OR '
@@ -589,14 +583,14 @@ function uniqueByMessage(
}
function isObject(
spec: CT.ListValueSpecOf<any>,
): spec is CT.ListValueSpecObject {
spec: IST.ListValueSpecOf<any>,
): spec is IST.ListValueSpecObject {
// only lists of objects have uniqueBy
return 'uniqueBy' in spec
}
export function convertValuesRecursive(
configSpec: CT.InputSpec,
configSpec: IST.InputSpec,
group: UntypedFormGroup,
) {
Object.entries(configSpec).forEach(([key, valueSpec]) => {
@@ -626,7 +620,7 @@ export function convertValuesRecursive(
})
} else if (valueSpec.spec.type === 'object') {
controls.forEach(formGroup => {
const objectSpec = valueSpec.spec as CT.ListValueSpecObject
const objectSpec = valueSpec.spec as IST.ListValueSpecObject
convertValuesRecursive(objectSpec.spec, formGroup as UntypedFormGroup)
})
}

View File

@@ -231,19 +231,6 @@ export class MarketplaceService {
}
}
private async updateStoreName(
url: string,
oldName: string | undefined,
newName: string,
): Promise<void> {
if (oldName !== newName) {
this.api.setDbValue<string>(
['marketplace', 'knownHosts', url, 'name'],
newName,
)
}
}
// UI only
readonly updateErrors: Record<string, string> = {}
readonly updateQueue: Record<string, boolean> = {}

View File

@@ -33,7 +33,7 @@ export class PatchDbSource extends Observable<Update<DataModel>[]> {
private readonly stream$ = inject(AuthService).isVerified$.pipe(
switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)),
switchMap(({ dump, guid }) =>
this.api.openWebsocket$<Revision>(guid, {}).pipe(
this.api.openWebsocket$<Revision>(guid).pipe(
bufferTime(250),
filter(revisions => !!revisions.length),
startWith([dump]),

View File

@@ -17,7 +17,7 @@ export function renderPkgStatus(
let health: T.HealthStatus | null = null
if (pkg.stateInfo.state === 'installed') {
primary = getInstalledPrimaryStatus(pkg.status)
primary = getInstalledPrimaryStatus(pkg)
dependency = getDependencyStatus(depErrors)
health = getHealthStatus(pkg.status)
} else {
@@ -27,11 +27,15 @@ export function renderPkgStatus(
return { primary, dependency, health }
}
function getInstalledPrimaryStatus(status: T.Status): PrimaryStatus {
if (!status.configured) {
return 'needsConfig'
function getInstalledPrimaryStatus(pkg: T.PackageDataEntry): PrimaryStatus {
if (
Object.values(pkg.requestedActions).some(
r => r.active && r.request.severity === 'critical',
)
) {
return 'actionRequired'
} else {
return status.main.status
return pkg.status.main
}
}
@@ -39,12 +43,12 @@ function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
return Object.values(depErrors).some(err => !!err) ? 'warning' : 'satisfied'
}
function getHealthStatus(status: T.Status): T.HealthStatus | null {
if (status.main.status !== 'running' || !status.main.health) {
function getHealthStatus(status: T.MainStatus): T.HealthStatus | null {
if (status.main !== 'running' || !status.main) {
return null
}
const values = Object.values(status.main.health)
const values = Object.values(status.health)
if (values.some(h => h.result === 'failure')) {
return 'failure'
@@ -78,7 +82,8 @@ export type PrimaryStatus =
| 'restarting'
| 'stopped'
| 'backingUp'
| 'needsConfig'
| 'actionRequired'
| 'error'
export type DependencyStatus = 'warning' | 'satisfied'
@@ -133,11 +138,16 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
color: 'success',
showDots: false,
},
needsConfig: {
display: 'Needs Config',
actionRequired: {
display: 'Action Required',
color: 'warning',
showDots: false,
},
error: {
display: 'Service Launch Error',
color: 'danger',
showDots: false,
},
}
export const DependencyRendering: Record<DependencyStatus, StatusRendering> = {

View File

@@ -0,0 +1,85 @@
import { Injectable } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { hasCurrentDeps } from '../util/has-deps'
import { getAllPackages } from '../util/get-package-data'
import { PatchDB } from 'patch-db-client'
import { DataModel } from './patch-db/data-model'
import { AlertController, NavController } from '@ionic/angular'
import { ApiService } from './api/embassy-api.service'
import { ErrorService, LoadingService } from '@start9labs/shared'
@Injectable({
providedIn: 'root',
})
export class StandardActionsService {
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly api: ApiService,
private readonly alertCtrl: AlertController,
private readonly errorService: ErrorService,
private readonly loader: LoadingService,
private readonly navCtrl: NavController,
) {}
async rebuild(id: string) {
const loader = this.loader.open(`Rebuilding Container...`).subscribe()
try {
await this.api.rebuildPackage({ id })
this.navCtrl.navigateBack('/services/' + id)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
async tryUninstall(manifest: T.Manifest): Promise<void> {
const { id, title, alerts } = manifest
let message =
alerts.uninstall ||
`Uninstalling ${title} will permanently delete its data`
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
message = `${message}. Services that depend on ${title} will no longer work properly and may crash`
}
const alert = await this.alertCtrl.create({
header: 'Warning',
message,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Uninstall',
handler: () => {
this.uninstall(id)
},
cssClass: 'enter-click',
},
],
cssClass: 'alert-warning-message',
})
await alert.present()
}
private async uninstall(id: string) {
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
try {
await this.api.uninstallPackage({ id })
this.api
.setDbValue<boolean>(['ackInstructions', id], false)
.catch(e => console.error('Failed to mark instructions as unseen', e))
this.navCtrl.navigateRoot('/services')
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
}