Convert properties to an action (#2751)

* update actions response types and partially implement in UI

* further remove diagnostic ui

* convert action response nested to array

* prepare action res modal for Alex

* ad dproperties action for Bitcoin

* feat: add action success dialog (#2753)

* feat: add action success dialog

* mocks for string action res and hide properties from actions page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* return null

* remove properties from backend

* misc fixes

* make severity separate argument

* rename ActionRequest to ActionRequestOptions

* add clearRequests

* fix s9pk build

* remove config and properties, introduce action requests

* better ux, better moocks, include icons

* fix dependency types

* add variant for versionCompat

* fix dep icon display and patch operation display

* misc fixes

* misc fixes

* alpha 12

* honor provided input to set values in action

* fix: show full descriptions of action success items (#2758)

* fix type

* fix: fix build:deps command on Windows (#2752)

* fix: fix build:deps command on Windows

* fix: add escaped quotes

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>

* misc db compatibility fixes

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
Matt Hill
2024-10-17 13:31:56 -06:00
committed by GitHub
parent fb074c8c32
commit 2ba56b8c59
105 changed files with 1385 additions and 1578 deletions

View File

@@ -150,15 +150,15 @@ export function makeEffects(context: EffectContext): Effects {
stack: new Error().stack,
}) as ReturnType<T.Effects["bind"]>
},
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
return rpcRound("clear-bindings", {}) as ReturnType<
clearBindings(...[options]: Parameters<T.Effects["clearBindings"]>) {
return rpcRound("clear-bindings", { ...options }) as ReturnType<
T.Effects["clearBindings"]
>
},
clearServiceInterfaces(
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
...[options]: Parameters<T.Effects["clearServiceInterfaces"]>
) {
return rpcRound("clear-service-interfaces", {}) as ReturnType<
return rpcRound("clear-service-interfaces", { ...options }) as ReturnType<
T.Effects["clearServiceInterfaces"]
>
},

View File

@@ -42,6 +42,7 @@ export const matchRpcResult = anyOf(
),
}),
)
export type RpcResult = typeof matchRpcResult._TYPE
type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null
@@ -88,7 +89,7 @@ const sandboxRunType = object(
const callbackType = object({
method: literal("callback"),
params: object({
callback: number,
id: number,
args: array,
}),
})
@@ -288,8 +289,8 @@ export class RpcListener {
return handleRpc(id, result)
})
.when(callbackType, async ({ params: { callback, args } }) => {
this.callCallback(callback, args)
.when(callbackType, async ({ params: { id, args } }) => {
this.callCallback(id, args)
return null
})
.when(startType, async ({ id }) => {
@@ -410,7 +411,7 @@ export class RpcListener {
input: any,
) {
const ensureResultTypeShape = (
result: void | T.ActionInput | T.PropertiesReturn | T.ActionResult | null,
result: void | T.ActionInput | T.ActionResult | null,
): { result: any } => {
if (isResult(result)) return result
return { result }
@@ -428,8 +429,6 @@ export class RpcListener {
return system.createBackup(effects, timeout || null)
case "/backup/restore":
return system.restoreBackup(effects, timeout || null)
case "/properties":
return system.properties(effects, timeout || null)
case "/packageInit":
return system.packageInit(effects, timeout || null)
case "/packageUninit":

View File

@@ -135,6 +135,34 @@ type OldGetConfigRes = {
spec: OldConfigSpec
}
export type PropertiesValue =
| {
/** The type of this value, either "string" or "object" */
type: "object"
/** A nested mapping of values. The user will experience this as a nested page with back button */
value: { [k: string]: PropertiesValue }
/** (optional) A human readable description of the new set of values */
description: string | null
}
| {
/** The type of this value, either "string" or "object" */
type: "string"
/** The value to display to the user */
value: string
/** A human readable description of the value */
description: string | null
/** Whether or not to mask the value, for example, when displaying a password */
masked: boolean | null
/** Whether or not to include a button for copying the value to clipboard */
copyable: boolean | null
/** Whether or not to include a button for displaying the value as a QR code */
qr: boolean | null
}
export type PropertiesReturn = {
[key: string]: PropertiesValue
}
export type PackagePropertiesV2 = {
[name: string]: PackagePropertyObject | PackagePropertyString
}
@@ -157,7 +185,7 @@ export type PackagePropertyObject = {
const asProperty_ = (
x: PackagePropertyString | PackagePropertyObject,
): T.PropertiesValue => {
): PropertiesValue => {
if (x.type === "object") {
return {
...x,
@@ -177,7 +205,7 @@ const asProperty_ = (
...x,
}
}
const asProperty = (x: PackagePropertiesV2): T.PropertiesReturn =>
const asProperty = (x: PackagePropertiesV2): PropertiesReturn =>
Object.fromEntries(
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
)
@@ -214,6 +242,31 @@ const matchProperties = object({
data: matchPackageProperties,
})
function convertProperties(
name: string,
value: PropertiesValue,
): T.ActionResultV1 {
if (value.type === "string") {
return {
type: "string",
name,
description: value.description,
copyable: value.copyable || false,
masked: value.masked || false,
qr: value.qr || false,
value: value.value,
}
}
return {
type: "object",
name,
description: value.description || undefined,
value: Object.entries(value.value).map(([name, value]) =>
convertProperties(name, value),
),
}
}
const DEFAULT_REGISTRY = "https://registry.start9.com"
export class SystemForEmbassy implements System {
currentRunning: MainLoop | undefined
@@ -245,6 +298,9 @@ export class SystemForEmbassy implements System {
await this.dependenciesAutoconfig(effects, depId, null)
}
}
await effects.setMainStatus({ status: "stopped" })
await this.exportActions(effects)
await this.exportNetwork(effects)
}
async exit(): Promise<void> {
@@ -281,10 +337,15 @@ export class SystemForEmbassy implements System {
await effects.setDataVersion({
version: ExtendedVersion.parseEmver(this.manifest.version).toString(),
})
} else {
await effects.action.request({
packageId: this.manifest.id,
actionId: "config",
severity: "critical",
replayId: "needs-config",
reason: "This service must be configured before it can be run",
})
}
await effects.setMainStatus({ status: "stopped" })
await this.exportActions(effects)
await this.exportNetwork(effects)
}
async exportNetwork(effects: Effects) {
for (const [id, interfaceValue] of Object.entries(
@@ -375,6 +436,8 @@ export class SystemForEmbassy implements System {
if (actionId === "config") {
const config = await this.getConfig(effects, timeoutMs)
return { spec: config.spec, value: config.config }
} else if (actionId === "properties") {
return null
} else {
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
if (!oldSpec) return null
@@ -393,6 +456,17 @@ export class SystemForEmbassy implements System {
if (actionId === "config") {
await this.setConfig(effects, input, timeoutMs)
return null
} else if (actionId === "properties") {
return {
version: "1",
type: "object",
name: "Properties",
description:
"Runtime information, credentials, and other values of interest",
value: Object.entries(await this.properties(effects, timeoutMs)).map(
([name, value]) => convertProperties(name, value),
),
}
} else {
return this.action(effects, actionId, input, timeoutMs)
}
@@ -405,17 +479,21 @@ export class SystemForEmbassy implements System {
if (manifest.config) {
actions.config = {
name: "Configure",
description: "Edit the configuration of this service",
description: `Customize ${manifest.title}`,
"allowed-statuses": ["running", "stopped"],
"input-spec": {},
implementation: { type: "script", args: [] },
}
await effects.action.request({
packageId: this.manifest.id,
actionId: "config",
replayId: "needs-config",
description: "This service must be configured before it can be run",
})
}
if (manifest.properties) {
actions.properties = {
name: "Properties",
description:
"Runtime information, credentials, and other values of interest",
"allowed-statuses": ["running", "stopped"],
"input-spec": null,
implementation: { type: "script", args: [] },
}
}
for (const [actionId, action] of Object.entries(actions)) {
const hasRunning = !!action["allowed-statuses"].find(
@@ -694,7 +772,7 @@ export class SystemForEmbassy implements System {
async properties(
effects: Effects,
timeoutMs: number | null,
): Promise<T.PropertiesReturn> {
): Promise<PropertiesReturn> {
// TODO BLU-J set the properties ever so often
const setConfigValue = this.manifest.properties
if (!setConfigValue) throw new Error("There is no properties")
@@ -867,7 +945,8 @@ export class SystemForEmbassy implements System {
actionId: "config",
packageId: id,
replayId: `${id}/config`,
description: `Configure this dependency for the needs of ${this.manifest.title}`,
severity: "important",
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
input: {
kind: "partial",
value: diff.diff,

View File

@@ -57,12 +57,6 @@ export class SystemForStartOs implements System {
effects,
}))
}
properties(
effects: Effects,
timeoutMs: number | null,
): Promise<T.PropertiesReturn> {
throw new Error("Method not implemented.")
}
getActionInput(
effects: Effects,
id: string,

View File

@@ -8,7 +8,6 @@ export type Procedure =
| "/packageUninit"
| "/backup/create"
| "/backup/restore"
| "/properties"
| `/actions/${string}/getInput`
| `/actions/${string}/run`
@@ -30,10 +29,6 @@ export type System = {
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
properties(
effects: Effects,
timeoutMs: number | null,
): Promise<T.PropertiesReturn>
runAction(
effects: Effects,
actionId: string,

View File

@@ -1,6 +1,6 @@
import { T } from "@start9labs/start-sdk"
const CallbackIdCell = { inc: 0 }
const CallbackIdCell = { inc: 1 }
const callbackRegistry = new FinalizationRegistry(
async (options: { cbs: Map<number, Function>; effects: T.Effects }) => {
@@ -23,6 +23,7 @@ export class CallbackHolder {
return
}
const id = this.newId()
console.error("adding callback", id)
this.callbacks.set(id, callback)
if (this.effects)
callbackRegistry.register(this, {

View File

@@ -23,8 +23,6 @@ export const jsonPath = some(
"/packageUninit",
"/backup/create",
"/backup/restore",
"/actions/metadata",
"/properties",
),
string.refine(isNestedPath, "isNestedPath"),
)