From 2ba56b8c59a546fae783bc28e3adb174e0def4be Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 17 Oct 2024 13:31:56 -0600 Subject: [PATCH] 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 * 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 * misc db compatibility fixes --------- Co-authored-by: Alex Inkin Co-authored-by: Aiden McClelland Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- .../src/Adapters/EffectCreator.ts | 8 +- container-runtime/src/Adapters/RpcListener.ts | 11 +- .../Systems/SystemForEmbassy/index.ts | 107 ++++++++-- .../src/Adapters/Systems/SystemForStartOs.ts | 6 - container-runtime/src/Interfaces/System.ts | 5 - .../src/Models/CallbackHolder.ts | 3 +- container-runtime/src/Models/JsonPath.ts | 2 - core/models/src/procedure_name.rs | 2 - core/startos/src/action.rs | 99 +++++++++- core/startos/src/db/mod.rs | 24 ++- core/startos/src/db/model/package.rs | 19 +- core/startos/src/lib.rs | 10 - core/startos/src/logs.rs | 2 +- core/startos/src/properties.rs | 35 ---- core/startos/src/service/effects/net/bind.rs | 1 + core/startos/src/service/mod.rs | 2 +- .../src/service/persistent_container.rs | 34 +++- core/startos/src/service/properties.rs | 23 --- core/startos/src/util/logger.rs | 8 +- core/startos/src/version/mod.rs | 14 +- sdk/Makefile | 7 +- sdk/base/lib/Effects.ts | 2 +- sdk/base/lib/actions/index.ts | 58 +++++- sdk/base/lib/actions/setupActions.ts | 10 +- sdk/base/lib/dependencies/Dependency.ts | 21 -- .../lib/dependencies/setupDependencies.ts | 41 +--- sdk/base/lib/osBindings/ActionRequest.ts | 4 +- sdk/base/lib/osBindings/ActionResult.ts | 7 + sdk/base/lib/osBindings/ActionResultV0.ts | 8 + sdk/base/lib/osBindings/ActionResultV1.ts | 18 ++ sdk/base/lib/osBindings/ActionSeverity.ts | 3 + sdk/base/lib/osBindings/ActionVisibility.ts | 5 +- .../lib/osBindings/RequestActionParams.ts | 4 +- sdk/base/lib/osBindings/index.ts | 4 + sdk/base/lib/types.ts | 68 ------- sdk/base/package.json | 4 +- sdk/package/lib/StartSdk.ts | 107 +++------- sdk/package/lib/backup/Backups.ts | 8 +- sdk/package/lib/backup/setupBackups.ts | 4 +- sdk/package/lib/index.ts | 2 +- sdk/package/lib/inits/setupInit.ts | 8 +- sdk/package/lib/inits/setupInstall.ts | 10 +- sdk/package/lib/inits/setupUninstall.ts | 10 +- sdk/package/lib/mainFn/CommandController.ts | 2 +- sdk/package/lib/mainFn/Daemon.ts | 2 +- sdk/package/lib/mainFn/Daemons.ts | 8 +- sdk/package/lib/mainFn/Mounts.ts | 6 +- sdk/package/lib/mainFn/index.ts | 2 +- sdk/package/lib/manifest/setupManifest.ts | 18 +- sdk/package/lib/test/inputSpecBuilder.test.ts | 71 +++---- sdk/package/lib/test/output.sdk.ts | 75 +++---- sdk/package/lib/util/fileHelper.ts | 27 ++- sdk/package/package-lock.json | 33 ++-- sdk/package/package.json | 6 +- web/README.md | 2 + web/ionic.config.json | 6 - web/lint-staged.config.js | 1 - web/package.json | 5 +- .../src/app/pages/success/success.page.ts | 2 +- .../shared/src/types/workspace-config.ts | 2 +- .../app/app/preloader/preloader.component.ts | 1 - .../ui/src/app/components/form.component.ts | 7 +- .../src/app/modals/action-input.component.ts | 67 ++++--- ...t.ts => action-request-input.component.ts} | 33 ++-- .../action-success-group.component.ts | 54 +++++ .../action-success-item.component.ts | 184 ++++++++++++++++++ .../action-success/action-success.module.ts | 12 -- .../action-success/action-success.page.html | 35 ---- .../action-success/action-success.page.scss | 0 .../action-success/action-success.page.ts | 63 +++--- .../app-actions/app-actions.module.ts | 2 - .../app-actions/app-actions.page.html | 4 +- .../app-actions/app-actions.page.ts | 22 +-- .../app-metrics/app-metrics.module.ts | 26 --- .../app-metrics/app-metrics.page.html | 25 --- .../app-metrics/app-metrics.page.scss | 3 - .../app-metrics/app-metrics.page.ts | 59 ------ .../app-properties/app-properties.module.ts | 32 --- .../app-properties/app-properties.page.html | 119 ----------- .../app-properties/app-properties.page.scss | 3 - .../app-properties/app-properties.page.ts | 147 -------------- .../apps-routes/app-show/app-show.module.ts | 2 + .../apps-routes/app-show/app-show.page.html | 6 + .../apps-routes/app-show/app-show.page.ts | 100 ++-------- .../app-show-action-requests.component.html | 45 +++++ .../app-show-action-requests.component.scss | 16 ++ .../app-show-action-requests.component.ts | 94 +++++++++ .../app-show-dependencies.component.html | 6 +- .../app-show-status.component.html | 10 - .../app-show-status.component.ts | 23 +-- .../app-show/pipes/to-buttons.pipe.ts | 48 +---- .../pages/apps-routes/apps-routing.module.ts | 14 -- .../ui/src/app/services/action.service.ts | 100 ++++------ .../ui/src/app/services/api/api.fixures.ts | 144 +++++++++----- .../ui/src/app/services/api/api.types.ts | 19 +- .../app/services/api/embassy-api.service.ts | 10 +- .../services/api/embassy-live-api.service.ts | 17 +- .../services/api/embassy-mock-api.service.ts | 33 ++-- .../ui/src/app/services/api/mock-patch.ts | 67 ++++++- .../ui/src/app/services/dep-error.service.ts | 6 +- .../services/pkg-status-rendering.service.ts | 15 +- web/projects/ui/src/app/util/dep-info.ts | 30 +++ .../ui/src/app/util/get-package-data.ts | 12 -- .../ui/src/app/util/get-package-info.ts | 2 +- .../ui/src/app/util/properties.util.ts | 150 -------------- 105 files changed, 1385 insertions(+), 1578 deletions(-) delete mode 100644 core/startos/src/properties.rs delete mode 100644 core/startos/src/service/properties.rs delete mode 100644 sdk/base/lib/dependencies/Dependency.ts create mode 100644 sdk/base/lib/osBindings/ActionResult.ts create mode 100644 sdk/base/lib/osBindings/ActionResultV0.ts create mode 100644 sdk/base/lib/osBindings/ActionResultV1.ts create mode 100644 sdk/base/lib/osBindings/ActionSeverity.ts rename web/projects/ui/src/app/modals/{action-dep.component.ts => action-request-input.component.ts} (77%) create mode 100644 web/projects/ui/src/app/modals/action-success/action-success-group.component.ts create mode 100644 web/projects/ui/src/app/modals/action-success/action-success-item.component.ts delete mode 100644 web/projects/ui/src/app/modals/action-success/action-success.module.ts delete mode 100644 web/projects/ui/src/app/modals/action-success/action-success.page.html delete mode 100644 web/projects/ui/src/app/modals/action-success/action-success.page.scss delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss delete mode 100644 web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts create mode 100644 web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html create mode 100644 web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss create mode 100644 web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts create mode 100644 web/projects/ui/src/app/util/dep-info.ts delete mode 100644 web/projects/ui/src/app/util/properties.util.ts diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index db176536f..4ec3a5b7d 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -150,15 +150,15 @@ export function makeEffects(context: EffectContext): Effects { stack: new Error().stack, }) as ReturnType }, - clearBindings(...[]: Parameters) { - return rpcRound("clear-bindings", {}) as ReturnType< + clearBindings(...[options]: Parameters) { + return rpcRound("clear-bindings", { ...options }) as ReturnType< T.Effects["clearBindings"] > }, clearServiceInterfaces( - ...[]: Parameters + ...[options]: Parameters ) { - return rpcRound("clear-service-interfaces", {}) as ReturnType< + return rpcRound("clear-service-interfaces", { ...options }) as ReturnType< T.Effects["clearServiceInterfaces"] > }, diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index de2bb7f1a..f84f7b282 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -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": diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index bed30b11a..ae55eb690 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -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 { @@ -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 { + ): Promise { // 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, diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index c000b391b..51b7c24b8 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -57,12 +57,6 @@ export class SystemForStartOs implements System { effects, })) } - properties( - effects: Effects, - timeoutMs: number | null, - ): Promise { - throw new Error("Method not implemented.") - } getActionInput( effects: Effects, id: string, diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 14ca002ca..63781cfbd 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -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 restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise - properties( - effects: Effects, - timeoutMs: number | null, - ): Promise runAction( effects: Effects, actionId: string, diff --git a/container-runtime/src/Models/CallbackHolder.ts b/container-runtime/src/Models/CallbackHolder.ts index e1034b473..ce474268a 100644 --- a/container-runtime/src/Models/CallbackHolder.ts +++ b/container-runtime/src/Models/CallbackHolder.ts @@ -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; 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, { diff --git a/container-runtime/src/Models/JsonPath.ts b/container-runtime/src/Models/JsonPath.ts index a231adba7..d101836da 100644 --- a/container-runtime/src/Models/JsonPath.ts +++ b/container-runtime/src/Models/JsonPath.ts @@ -23,8 +23,6 @@ export const jsonPath = some( "/packageUninit", "/backup/create", "/backup/restore", - "/actions/metadata", - "/properties", ), string.refine(isNestedPath, "isNestedPath"), ) diff --git a/core/models/src/procedure_name.rs b/core/models/src/procedure_name.rs index 9f8a6c0dd..4a4a682e6 100644 --- a/core/models/src/procedure_name.rs +++ b/core/models/src/procedure_name.rs @@ -7,7 +7,6 @@ pub enum ProcedureName { GetConfig, SetConfig, CreateBackup, - Properties, RestoreBackup, GetActionInput(ActionId), RunAction(ActionId), @@ -23,7 +22,6 @@ impl ProcedureName { ProcedureName::SetConfig => "/config/set".to_string(), ProcedureName::GetConfig => "/config/get".to_string(), ProcedureName::CreateBackup => "/backup/create".to_string(), - ProcedureName::Properties => "/properties".to_string(), ProcedureName::RestoreBackup => "/backup/restore".to_string(), ProcedureName::RunAction(id) => format!("/actions/{}/run", id), ProcedureName::GetActionInput(id) => format!("/actions/{}/getInput", id), diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index a18173add..3f1912c34 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -6,6 +6,7 @@ use models::PackageId; use qrcode::QrCode; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use tracing::instrument; use ts_rs::TS; @@ -74,21 +75,25 @@ pub async fn get_action_input( .await } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, TS)] #[serde(tag = "version")] +#[ts(export)] pub enum ActionResult { #[serde(rename = "0")] V0(ActionResultV0), + #[serde(rename = "1")] + V1(ActionResultV1), } impl fmt::Display for ActionResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::V0(res) => res.fmt(f), + Self::V1(res) => res.fmt(f), } } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, TS)] pub struct ActionResultV0 { pub message: String, pub value: Option, @@ -116,6 +121,96 @@ impl fmt::Display for ActionResultV0 { } } +#[derive(Debug, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[serde(rename_all_fields = "camelCase")] +#[serde(tag = "type")] +pub enum ActionResultV1 { + String { + name: String, + value: String, + description: Option, + copyable: bool, + qr: bool, + masked: bool, + }, + Object { + name: String, + value: Vec, + #[ts(optional)] + description: Option, + }, +} +impl ActionResultV1 { + fn fmt_rec(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result { + match self { + Self::String { + name, + value, + description, + qr, + .. + } => { + for i in 0..indent { + write!(f, " ")?; + } + write!(f, "{name}")?; + if let Some(description) = description { + write!(f, ": {description}")?; + } + if !value.is_empty() { + write!(f, ":\n")?; + for i in 0..indent { + write!(f, " ")?; + } + write!(f, "{value}")?; + if *qr { + use qrcode::render::unicode; + write!(f, "\n")?; + for i in 0..indent { + write!(f, " ")?; + } + write!( + f, + "{}", + QrCode::new(value.as_bytes()) + .unwrap() + .render::() + .build() + )?; + } + } + } + Self::Object { + name, + value, + description, + } => { + for i in 0..indent { + write!(f, " ")?; + } + write!(f, "{name}")?; + if let Some(description) = description { + write!(f, ": {description}")?; + } + for value in value { + write!(f, ":\n")?; + for i in 0..indent { + write!(f, " ")?; + } + value.fmt_rec(f, indent + 1)?; + } + } + } + Ok(()) + } +} +impl fmt::Display for ActionResultV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt_rec(f, 0) + } +} + pub fn display_action_result(params: WithIoFormat, result: Option) { let Some(result) = result else { return; diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index 1ed7e9ab9..c7e38e81c 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -228,6 +228,8 @@ pub async fn subscribe( #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct CliApplyParams { + #[arg(long)] + allow_model_mismatch: bool, expr: String, path: Option, } @@ -238,7 +240,12 @@ async fn cli_apply( context, parent_method, method, - params: CliApplyParams { expr, path }, + params: + CliApplyParams { + allow_model_mismatch, + expr, + path, + }, .. }: HandlerArgs, ) -> Result<(), RpcError> { @@ -253,7 +260,14 @@ async fn cli_apply( &expr, )?; - Ok::<_, Error>(( + let value = if allow_model_mismatch { + serde_json::from_value::(res.clone().into()).with_ctx(|_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + })? + } else { to_value( &serde_json::from_value::(res.clone().into()).with_ctx( |_| { @@ -263,9 +277,9 @@ async fn cli_apply( ) }, )?, - )?, - (), - )) + )? + }; + Ok::<_, Error>((value, ())) }) .await?; } else { diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 5fb6b9655..df5773cc3 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -338,7 +338,7 @@ pub struct ActionMetadata { #[serde(rename_all_fields = "camelCase")] pub enum ActionVisibility { Hidden, - Disabled { reason: String }, + Disabled(String), Enabled, } impl Default for ActionVisibility { @@ -444,14 +444,29 @@ pub struct ActionRequestEntry { pub struct ActionRequest { pub package_id: PackageId, pub action_id: ActionId, + #[serde(default)] + pub severity: ActionSeverity, #[ts(optional)] - pub description: Option, + pub reason: Option, #[ts(optional)] pub when: Option, #[ts(optional)] pub input: Option, } +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "kebab-case")] +#[ts(export)] +pub enum ActionSeverity { + Critical, + Important, +} +impl Default for ActionSeverity { + fn default() -> Self { + ActionSeverity::Important + } +} + #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 754362bc9..d9d626754 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -49,7 +49,6 @@ pub mod notifications; pub mod os_install; pub mod prelude; pub mod progress; -pub mod properties; pub mod registry; pub mod rpc_continuations; pub mod s9pk; @@ -395,15 +394,6 @@ pub fn package() -> ParentHandler { .no_display() .with_about("Display package logs"), ) - .subcommand( - "properties", - from_fn_async(properties::properties) - .with_custom_display_fn(|_handle, result| { - Ok(properties::display_properties(result)) - }) - .with_about("Display package Properties") - .with_call_remote::(), - ) .subcommand( "backup", backup::package_backup::() diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 7ff1d7d08..2db7e9952 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -113,7 +113,7 @@ async fn ws_handler( #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct LogResponse { - entries: Reversible, + pub entries: Reversible, start_cursor: Option, end_cursor: Option, } diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs deleted file mode 100644 index e24b14965..000000000 --- a/core/startos/src/properties.rs +++ /dev/null @@ -1,35 +0,0 @@ -use clap::Parser; -use imbl_value::{json, Value}; -use models::PackageId; -use serde::{Deserialize, Serialize}; - -use crate::context::RpcContext; -use crate::prelude::*; -use crate::Error; - -pub fn display_properties(response: Value) { - println!("{}", response); -} - -#[derive(Deserialize, Serialize, Parser)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct PropertiesParam { - id: PackageId, -} -// #[command(display(display_properties))] -pub async fn properties( - ctx: RpcContext, - PropertiesParam { id }: PropertiesParam, -) -> Result { - match &*ctx.services.get(&id).await { - Some(service) => Ok(json!({ - "version": 2, - "data": service.properties().await? - })), - None => Err(Error::new( - eyre!("Could not find a service with id {id}"), - ErrorKind::NotFound, - )), - } -} diff --git a/core/startos/src/service/effects/net/bind.rs b/core/startos/src/service/effects/net/bind.rs index 40bc550cb..5619375eb 100644 --- a/core/startos/src/service/effects/net/bind.rs +++ b/core/startos/src/service/effects/net/bind.rs @@ -32,6 +32,7 @@ pub async fn bind( #[ts(export)] #[serde(rename_all = "camelCase")] pub struct ClearBindingsParams { + #[serde(default)] pub except: Vec, } diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index d7b91df7b..cc31efe11 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -55,7 +55,6 @@ pub mod cli; mod control; pub mod effects; pub mod persistent_container; -mod properties; mod rpc; mod service_actor; pub mod service_map; @@ -132,6 +131,7 @@ impl ServiceRef { ); Ok(()) })?; + d.as_private_mut().as_package_stores_mut().remove(&id)?; Ok(Some(pde)) } else { Ok(None) diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 3f85a95d5..13cb7688c 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::ops::Deref; use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -379,11 +380,7 @@ impl PersistentContainer { )); } - self.rpc_client - .request(rpc::Init, Empty {}) - .await - .map_err(Error::from) - .log_err(); + self.rpc_client.request(rpc::Init, Empty {}).await?; self.state.send_modify(|s| s.rt_initialized = true); @@ -391,7 +388,10 @@ impl PersistentContainer { } #[instrument(skip_all)] - fn destroy(&mut self) -> Option> + 'static> { + fn destroy( + &mut self, + error: bool, + ) -> Option> + 'static> { if self.destroyed { return None; } @@ -406,6 +406,24 @@ impl PersistentContainer { self.destroyed = true; Some(async move { let mut errs = ErrorCollection::new(); + if error { + if let Some(lxc_container) = &lxc_container { + if let Some(logs) = errs.handle( + crate::logs::fetch_logs( + crate::logs::LogSource::Container(lxc_container.guid.deref().clone()), + Some(50), + None, + None, + false, + ) + .await, + ) { + for log in logs.entries.iter() { + eprintln!("{log}"); + } + } + } + } if let Some((hdl, shutdown)) = rpc_server { errs.handle(rpc_client.request(rpc::Exit, Empty {}).await); shutdown.shutdown(); @@ -433,7 +451,7 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn exit(mut self) -> Result<(), Error> { - if let Some(destroy) = self.destroy() { + if let Some(destroy) = self.destroy(false) { dbg!(destroy.await)?; } tracing::info!("Service for {} exited", self.s9pk.as_manifest().id); @@ -551,7 +569,7 @@ impl PersistentContainer { impl Drop for PersistentContainer { fn drop(&mut self) { - if let Some(destroy) = self.destroy() { + if let Some(destroy) = self.destroy(true) { tokio::spawn(async move { destroy.await.log_err() }); } } diff --git a/core/startos/src/service/properties.rs b/core/startos/src/service/properties.rs deleted file mode 100644 index 3f5201f1d..000000000 --- a/core/startos/src/service/properties.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::time::Duration; - -use models::ProcedureName; - -use crate::prelude::*; -use crate::rpc_continuations::Guid; -use crate::service::Service; - -impl Service { - // TODO: leave here or switch to Actor Message? - pub async fn properties(&self) -> Result { - let container = &self.seed.persistent_container; - container - .execute::( - Guid::new(), - ProcedureName::Properties, - Value::Null, - Some(Duration::from_secs(30)), - ) - .await - .with_kind(ErrorKind::Unknown) - } -} diff --git a/core/startos/src/util/logger.rs b/core/startos/src/util/logger.rs index c7ab41ba2..c464b328d 100644 --- a/core/startos/src/util/logger.rs +++ b/core/startos/src/util/logger.rs @@ -1,3 +1,5 @@ +use std::io; + use tracing::Subscriber; use tracing_subscriber::util::SubscriberInitExt; @@ -21,7 +23,11 @@ impl EmbassyLogger { let filter_layer = filter_layer .add_directive("tokio=trace".parse().unwrap()) .add_directive("runtime=trace".parse().unwrap()); - let fmt_layer = fmt::layer().with_target(true); + let fmt_layer = fmt::layer() + .with_writer(io::stderr) + .with_line_number(true) + .with_file(true) + .with_target(true); let sub = tracing_subscriber::registry() .with(filter_layer) diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 76cc7a574..ae203f3e5 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -171,9 +171,17 @@ fn version_accessor(db: &mut Value) -> Option<&mut Value> { fn version_compat_accessor(db: &mut Value) -> Option<&mut Value> { if db.get("public").is_some() { - db.get_mut("public")? - .get_mut("serverInfo")? - .get_mut("versionCompat") + let server_info = db.get_mut("public")?.get_mut("serverInfo")?; + if server_info.get("versionCompat").is_some() { + server_info.get_mut("versionCompat") + } else { + if let Some(prev) = server_info.get("eosVersionCompat").cloned() { + server_info + .as_object_mut()? + .insert("versionCompat".into(), prev); + } + server_info.get_mut("versionCompat") + } } else { db.get_mut("server-info")?.get_mut("eos-version-compat") } diff --git a/sdk/Makefile b/sdk/Makefile index bc025709d..3f8ae533a 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -24,7 +24,7 @@ clean: package/lib/test/output.ts: package/node_modules package/lib/test/makeOutput.ts package/scripts/oldSpecToBuilder.ts cd package && npm run buildOutput -bundle: dist baseDist | test fmt +bundle: baseDist dist | test fmt touch dist base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs @@ -67,9 +67,8 @@ base/node_modules: base/package.json node_modules: package/node_modules base/node_modules -publish: bundle package/package.json README.md LICENSE - cd dist - npm publish --access=public +publish: bundle package/package.json package/README.md package/LICENSE + cd dist && npm publish --access=public link: bundle cd dist && npm link diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts index 16d82464d..f985db77b 100644 --- a/sdk/base/lib/Effects.ts +++ b/sdk/base/lib/Effects.ts @@ -52,7 +52,7 @@ export type Effects = { options: RequestActionParams, ): Promise clearRequests( - options: { only: ActionId[] } | { except: ActionId[] }, + options: { only: string[] } | { except: string[] }, ): Promise } diff --git a/sdk/base/lib/actions/index.ts b/sdk/base/lib/actions/index.ts index cebb03ee1..2d052e072 100644 --- a/sdk/base/lib/actions/index.ts +++ b/sdk/base/lib/actions/index.ts @@ -1,5 +1,6 @@ import * as T from "../types" import * as IST from "../actions/input/inputSpecTypes" +import { Action } from "./setupActions" export type RunActionInput = | Input @@ -43,23 +44,62 @@ export const runAction = async < }) } } +type GetActionInputType< + A extends Action>, +> = A extends Action ? I : never -// prettier-ignore -export type ActionRequest> = - T extends { when: { condition: "input-not-matches" } } - ? (T extends { input: T.ActionRequestInput } ? T : "input is required for condition 'input-not-matches'") - : T +type ActionRequestBase = { + reason?: string + replayId?: string +} +type ActionRequestInput< + T extends Action>, +> = { + kind: "partial" + value: Partial> +} +export type ActionRequestOptions< + T extends Action>, +> = ActionRequestBase & + ( + | { + when?: Exclude< + T.ActionRequestTrigger, + { condition: "input-not-matches" } + > + input?: ActionRequestInput + } + | { + when: T.ActionRequestTrigger & { condition: "input-not-matches" } + input: ActionRequestInput + } + ) + +const _validate: T.ActionRequest = {} as ActionRequestOptions & { + actionId: string + packageId: string + severity: T.ActionSeverity +} export const requestAction = < - T extends Omit, + T extends Action>, >(options: { effects: T.Effects - request: ActionRequest & { replayId?: string; packageId: T.PackageId } + packageId: T.PackageId + action: T + severity: T.ActionSeverity + options?: ActionRequestOptions }) => { - const request = options.request + const request = options.options || {} + const actionId = options.action.id const req = { ...request, - replayId: request.replayId || `${request.packageId}:${request.actionId}`, + actionId, + packageId: options.packageId, + action: undefined, + severity: options.severity, + replayId: request.replayId || `${options.packageId}:${actionId}`, } + delete req.action return options.effects.action.request(req) } diff --git a/sdk/base/lib/actions/setupActions.ts b/sdk/base/lib/actions/setupActions.ts index 8c6b7734a..62d6cedd9 100644 --- a/sdk/base/lib/actions/setupActions.ts +++ b/sdk/base/lib/actions/setupActions.ts @@ -11,7 +11,7 @@ export type Run< > = (options: { effects: T.Effects input: ExtractInputSpecType & Record -}) => Promise +}) => Promise export type GetInput< A extends | Record @@ -19,7 +19,9 @@ export type GetInput< | InputSpec, never>, > = (options: { effects: T.Effects -}) => Promise & Record)> +}) => Promise< + null | void | undefined | (ExtractInputSpecType & Record) +> export type MaybeFn = T | ((options: { effects: T.Effects }) => Promise) function callMaybeFn( @@ -91,7 +93,7 @@ export class Action< ): Action { return new Action( id, - mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })), + mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })), {}, async () => null, run, @@ -114,7 +116,7 @@ export class Action< effects: T.Effects input: Type }): Promise { - return this.runFn(options) + return (await this.runFn(options)) || null } } diff --git a/sdk/base/lib/dependencies/Dependency.ts b/sdk/base/lib/dependencies/Dependency.ts deleted file mode 100644 index 8a1a862c7..000000000 --- a/sdk/base/lib/dependencies/Dependency.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { VersionRange } from "../exver" - -export class Dependency { - constructor( - readonly data: - | { - /** Either "running" or "exists". Does the dependency need to be running, or does it only need to exist? */ - type: "running" - /** The acceptable version range of the dependency. */ - versionRange: VersionRange - /** A list of the dependency's health check IDs that must be passing for the service to be satisfied. */ - healthChecks: string[] - } - | { - /** Either "running" or "exists". Does the dependency need to be running, or does it only need to exist? */ - type: "exists" - /** The acceptable version range of the dependency. */ - versionRange: VersionRange - }, - ) {} -} diff --git a/sdk/base/lib/dependencies/setupDependencies.ts b/sdk/base/lib/dependencies/setupDependencies.ts index 9d613ff74..f694c042c 100644 --- a/sdk/base/lib/dependencies/setupDependencies.ts +++ b/sdk/base/lib/dependencies/setupDependencies.ts @@ -1,22 +1,11 @@ import * as T from "../types" import { once } from "../util" -import { Dependency } from "./Dependency" -type DependencyType = { - [K in keyof { - [K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false - ? K - : never - }]: Dependency -} & { - [K in keyof { - [K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends true - ? K - : never - }]?: Dependency +type DependencyType = { + [K in keyof Manifest["dependencies"]]: Omit } -export function setupDependencies( +export function setupDependencies( fn: (options: { effects: T.Effects }) => Promise>, ): (options: { effects: T.Effects }) => Promise { const cell = { updater: async (_: { effects: T.Effects }) => null } @@ -30,24 +19,12 @@ export function setupDependencies( const dependencyType = await fn(options) return await options.effects.setDependencies({ dependencies: Object.entries(dependencyType).map( - ([ - id, - { - data: { versionRange, ...x }, - }, - ]) => ({ - id, - ...x, - ...(x.type === "running" - ? { - kind: "running", - healthChecks: x.healthChecks, - } - : { - kind: "exists", - }), - versionRange: versionRange.toString(), - }), + ([id, { versionRange, ...x }, ,]) => + ({ + id, + ...x, + versionRange: versionRange.toString(), + }) as T.DependencyRequirement, ), }) } diff --git a/sdk/base/lib/osBindings/ActionRequest.ts b/sdk/base/lib/osBindings/ActionRequest.ts index 63b5607d8..552f37bc6 100644 --- a/sdk/base/lib/osBindings/ActionRequest.ts +++ b/sdk/base/lib/osBindings/ActionRequest.ts @@ -2,12 +2,14 @@ import type { ActionId } from "./ActionId" import type { ActionRequestInput } from "./ActionRequestInput" import type { ActionRequestTrigger } from "./ActionRequestTrigger" +import type { ActionSeverity } from "./ActionSeverity" import type { PackageId } from "./PackageId" export type ActionRequest = { packageId: PackageId actionId: ActionId - description?: string + severity: ActionSeverity + reason?: string when?: ActionRequestTrigger input?: ActionRequestInput } diff --git a/sdk/base/lib/osBindings/ActionResult.ts b/sdk/base/lib/osBindings/ActionResult.ts new file mode 100644 index 000000000..7422dcde3 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResult.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionResultV0 } from "./ActionResultV0" +import type { ActionResultV1 } from "./ActionResultV1" + +export type ActionResult = + | ({ version: "0" } & ActionResultV0) + | ({ version: "1" } & ActionResultV1) diff --git a/sdk/base/lib/osBindings/ActionResultV0.ts b/sdk/base/lib/osBindings/ActionResultV0.ts new file mode 100644 index 000000000..7c6b43d45 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResultV0.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionResultV0 = { + message: string + value: string | null + copyable: boolean + qr: boolean +} diff --git a/sdk/base/lib/osBindings/ActionResultV1.ts b/sdk/base/lib/osBindings/ActionResultV1.ts new file mode 100644 index 000000000..cda0e5df9 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionResultV1.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionResultV1 = + | { + type: "string" + name: string + value: string + description: string | null + copyable: boolean + qr: boolean + masked: boolean + } + | { + type: "object" + name: string + value: Array + description?: string + } diff --git a/sdk/base/lib/osBindings/ActionSeverity.ts b/sdk/base/lib/osBindings/ActionSeverity.ts new file mode 100644 index 000000000..ad339f951 --- /dev/null +++ b/sdk/base/lib/osBindings/ActionSeverity.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ActionSeverity = "critical" | "important" diff --git a/sdk/base/lib/osBindings/ActionVisibility.ts b/sdk/base/lib/osBindings/ActionVisibility.ts index f7d5a2a2a..ab1e6e1b9 100644 --- a/sdk/base/lib/osBindings/ActionVisibility.ts +++ b/sdk/base/lib/osBindings/ActionVisibility.ts @@ -1,6 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ActionVisibility = - | "hidden" - | { disabled: { reason: string } } - | "enabled" +export type ActionVisibility = "hidden" | { disabled: string } | "enabled" diff --git a/sdk/base/lib/osBindings/RequestActionParams.ts b/sdk/base/lib/osBindings/RequestActionParams.ts index db83be595..ccc8d0e61 100644 --- a/sdk/base/lib/osBindings/RequestActionParams.ts +++ b/sdk/base/lib/osBindings/RequestActionParams.ts @@ -2,6 +2,7 @@ import type { ActionId } from "./ActionId" import type { ActionRequestInput } from "./ActionRequestInput" import type { ActionRequestTrigger } from "./ActionRequestTrigger" +import type { ActionSeverity } from "./ActionSeverity" import type { PackageId } from "./PackageId" import type { ReplayId } from "./ReplayId" @@ -9,7 +10,8 @@ export type RequestActionParams = { replayId: ReplayId packageId: PackageId actionId: ActionId - description?: string + severity: ActionSeverity + reason?: string when?: ActionRequestTrigger input?: ActionRequestInput } diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 4b6af0ae1..5d5574acf 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -7,6 +7,10 @@ export { ActionRequestEntry } from "./ActionRequestEntry" export { ActionRequestInput } from "./ActionRequestInput" export { ActionRequestTrigger } from "./ActionRequestTrigger" export { ActionRequest } from "./ActionRequest" +export { ActionResult } from "./ActionResult" +export { ActionResultV0 } from "./ActionResultV0" +export { ActionResultV1 } from "./ActionResultV1" +export { ActionSeverity } from "./ActionSeverity" export { ActionVisibility } from "./ActionVisibility" export { AddAdminParams } from "./AddAdminParams" export { AddAssetParams } from "./AddAssetParams" diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index 071647961..d7ab1e51b 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -33,10 +33,6 @@ export const SIGKILL: Signals = "SIGKILL" export const NO_TIMEOUT = -1 export type PathMaker = (options: { volume: string; path: string }) => string -export type ExportedAction = (options: { - effects: Effects - input?: Record -}) => Promise export type MaybePromise = Promise | A export namespace ExpectedExports { version: 1 @@ -86,10 +82,6 @@ export namespace ExpectedExports { nextVersion: null | string }) => Promise - export type properties = (options: { - effects: Effects - }) => Promise - export type manifest = Manifest export type actions = Actions< @@ -105,7 +97,6 @@ export type ABI = { containerInit: ExpectedExports.containerInit packageInit: ExpectedExports.packageInit packageUninit: ExpectedExports.packageUninit - properties: ExpectedExports.properties manifest: ExpectedExports.manifest actions: ExpectedExports.actions } @@ -177,58 +168,6 @@ export type ExposeServicePaths = { paths: ExposedStorePaths } -export type SdkPropertiesValue = - | { - type: "object" - value: { [k: string]: SdkPropertiesValue } - description?: string - } - | { - type: "string" - /** The value to display to the user */ - value: string - /** A human readable description or explanation of the value */ - description?: string - /** Whether or not to mask the value, for example, when displaying a password */ - masked?: boolean - /** Whether or not to include a button for copying the value to clipboard */ - copyable?: boolean - /** Whether or not to include a button for displaying the value as a QR code */ - qr?: boolean - } - -export type SdkPropertiesReturn = { - [key: string]: SdkPropertiesValue -} - -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 EffectMethod = { [K in keyof T]-?: K extends string ? T[K] extends Function @@ -264,13 +203,6 @@ export type Metadata = { mode: number } -export type ActionResult = { - version: "0" - message: string - value: string | null - copyable: boolean - qr: boolean -} export type SetResult = { dependsOn: DependsOn signal: Signals diff --git a/sdk/base/package.json b/sdk/base/package.json index 3d1d4b62e..e38cc5ac6 100644 --- a/sdk/base/package.json +++ b/sdk/base/package.json @@ -4,9 +4,9 @@ "types": "./index.d.ts", "sideEffects": true, "scripts": { - "peggy": "peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs", + "peggy": "peggy --allowed-start-rules \"*\" --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs", "test": "jest -c ./jest.config.js --coverage", - "buildOutput": "npx prettier --write '**/*.ts'", + "buildOutput": "npx prettier --write \"**/*.ts\"", "check": "tsc --noEmit", "tsc": "tsc" }, diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 412dafe52..12e92faf2 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -55,7 +55,6 @@ import { getStore } from "./store/getStore" import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer" import { splitCommand } from "./util" import { Mounts } from "./mainFn/Mounts" -import { Dependency } from "../../base/lib/dependencies/Dependency" import { setupDependencies } from "../../base/lib/dependencies/setupDependencies" import * as T from "../../base/lib/types" import { testTypeVersion } from "../../base/lib/exver" @@ -86,12 +85,12 @@ type AnyNeverCond = T extends [any, ...infer U] ? AnyNeverCond : never -export class StartSdk { +export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { return new StartSdk(null as never) } - withManifest(manifest: Manifest) { + withManifest(manifest: Manifest) { return new StartSdk(manifest) } withStore>() { @@ -141,17 +140,39 @@ export class StartSdk { ...startSdkEffectWrapper, action: { run: actions.runAction, - request: actions.requestAction, - requestOwn: >( + request: < + T extends Action>, + >( effects: T.Effects, - request: actions.ActionRequest & { - replayId?: string - }, + packageId: T.PackageId, + action: T, + severity: T.ActionSeverity, + options?: actions.ActionRequestOptions, ) => actions.requestAction({ effects, - request: { ...request, packageId: this.manifest.id }, + packageId, + action, + severity, + options: options, }), + requestOwn: < + T extends Action>, + >( + effects: T.Effects, + action: T, + severity: T.ActionSeverity, + options?: actions.ActionRequestOptions, + ) => + actions.requestAction({ + effects, + packageId: this.manifest.id, + action, + severity, + options: options, + }), + clearRequest: (effects: T.Effects, ...replayIds: string[]) => + effects.action.clearRequests({ only: replayIds }), }, checkDependencies: checkDependencies as < DependencyId extends keyof Manifest["dependencies"] & @@ -370,17 +391,6 @@ export class StartSdk { return healthCheck(o) }, }, - Dependency: { - /** - * @description Use this function to create a dependency for the service. - * @property {DependencyType} type - * @property {VersionRange} versionRange - * @property {string[]} healthChecks - */ - of(data: Dependency["data"]) { - return new Dependency({ ...data }) - }, - }, healthCheck: { checkPortListening, checkWebUrl, @@ -566,37 +576,6 @@ export class StartSdk { started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, ) => setupMain(fn), - /** - * @description Use this function to determine which information to expose to the UI in the "Properties" section. - * - * Values can be obtained from anywhere: the Store, the upstream service, or another service. - * @example - * In this example, we retrieve the admin password from the Store and expose it, masked and copyable, to - * the UI as "Admin Password". - * - * ``` - export const properties = sdk.setupProperties(async ({ effects }) => { - const store = await sdk.store.getOwn(effects, sdk.StorePath).once() - - return { - 'Admin Password': { - type: 'string', - value: store.adminPassword, - description: 'Used for logging into the admin UI', - copyable: true, - masked: true, - qr: false, - }, - } - }) - * ``` - */ - setupProperties: - ( - fn: (options: { effects: Effects }) => Promise, - ): T.ExpectedExports.properties => - (options) => - fn(options).then(nullifyProperties), /** * Use this function to execute arbitrary logic *once*, on uninstall only. Most services will not use this. */ @@ -1057,6 +1036,7 @@ export class StartSdk { * ``` */ list: Value.list, + hidden: Value.hidden, dynamicToggle: ( a: LazyBuild< Store, @@ -1367,7 +1347,7 @@ export class StartSdk { } } -export async function runCommand( +export async function runCommand( effects: Effects, image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: string | [string, ...string[]], @@ -1385,26 +1365,3 @@ export async function runCommand( (subcontainer) => subcontainer.exec(commands), ) } -function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn { - return Object.fromEntries( - Object.entries(value).map(([k, v]) => [k, nullifyProperties_(v)]), - ) -} -function nullifyProperties_(value: T.SdkPropertiesValue): T.PropertiesValue { - if (value.type === "string") { - return { - description: null, - copyable: null, - masked: null, - qr: null, - ...value, - } - } - return { - description: null, - ...value, - value: Object.fromEntries( - Object.entries(value.value).map(([k, v]) => [k, nullifyProperties_(v)]), - ), - } -} diff --git a/sdk/package/lib/backup/Backups.ts b/sdk/package/lib/backup/Backups.ts index 8d8f03295..c27f2be72 100644 --- a/sdk/package/lib/backup/Backups.ts +++ b/sdk/package/lib/backup/Backups.ts @@ -35,7 +35,7 @@ export type BackupSync = { * ).build()q * ``` */ -export class Backups { +export class Backups { private constructor( private options = DEFAULT_OPTIONS, private restoreOptions: Partial = {}, @@ -43,7 +43,7 @@ export class Backups { private backupSet = [] as BackupSync[], ) {} - static withVolumes( + static withVolumes( ...volumeNames: Array ): Backups { return Backups.withSyncs( @@ -54,13 +54,13 @@ export class Backups { ) } - static withSyncs( + static withSyncs( ...syncs: BackupSync[] ) { return syncs.reduce((acc, x) => acc.addSync(x), new Backups()) } - static withOptions( + static withOptions( options?: Partial, ) { return new Backups({ ...DEFAULT_OPTIONS, ...options }) diff --git a/sdk/package/lib/backup/setupBackups.ts b/sdk/package/lib/backup/setupBackups.ts index b9654824b..722d245ff 100644 --- a/sdk/package/lib/backup/setupBackups.ts +++ b/sdk/package/lib/backup/setupBackups.ts @@ -2,7 +2,7 @@ import { Backups } from "./Backups" import * as T from "../../../base/lib/types" import { _ } from "../util" -export type SetupBackupsParams = +export type SetupBackupsParams = | M["volumes"][number][] | ((_: { effects: T.Effects }) => Promise>) @@ -11,7 +11,7 @@ type SetupBackupsRes = { restoreBackup: T.ExpectedExports.restoreBackup } -export function setupBackups( +export function setupBackups( options: SetupBackupsParams, ) { let backupsFactory: (_: { effects: T.Effects }) => Promise> diff --git a/sdk/package/lib/index.ts b/sdk/package/lib/index.ts index a83e35745..3619765e1 100644 --- a/sdk/package/lib/index.ts +++ b/sdk/package/lib/index.ts @@ -28,7 +28,7 @@ export { export { Daemons } from "./mainFn/Daemons" export { SubContainer } from "./util/SubContainer" export { StartSdk } from "./StartSdk" -export { setupManifest } from "./manifest/setupManifest" +export { setupManifest, buildManifest } from "./manifest/setupManifest" export { FileHelper } from "./util/fileHelper" export { setupExposeStore } from "./store/setupExposeStore" export { pathBuilder } from "../../base/lib/util/PathBuilder" diff --git a/sdk/package/lib/inits/setupInit.ts b/sdk/package/lib/inits/setupInit.ts index af2503dad..8d30f5b1f 100644 --- a/sdk/package/lib/inits/setupInit.ts +++ b/sdk/package/lib/inits/setupInit.ts @@ -7,12 +7,14 @@ import { VersionGraph } from "../version/VersionGraph" import { Install } from "./setupInstall" import { Uninstall } from "./setupUninstall" -export function setupInit( - versions: VersionGraph, +export function setupInit( + versions: VersionGraph, install: Install, uninstall: Uninstall, setServiceInterfaces: UpdateServiceInterfaces, - setDependencies: (options: { effects: T.Effects }) => Promise, + setDependencies: (options: { + effects: T.Effects + }) => Promise, actions: Actions, exposedStore: ExposedStorePaths, ): { diff --git a/sdk/package/lib/inits/setupInstall.ts b/sdk/package/lib/inits/setupInstall.ts index 9b5e92afe..38a96a00b 100644 --- a/sdk/package/lib/inits/setupInstall.ts +++ b/sdk/package/lib/inits/setupInstall.ts @@ -1,11 +1,11 @@ import * as T from "../../../base/lib/types" -export type InstallFn = (opts: { +export type InstallFn = (opts: { effects: T.Effects -}) => Promise -export class Install { +}) => Promise +export class Install { private constructor(readonly fn: InstallFn) {} - static of( + static of( fn: InstallFn, ) { return new Install(fn) @@ -18,7 +18,7 @@ export class Install { } } -export function setupInstall( +export function setupInstall( fn: InstallFn, ) { return Install.of(fn) diff --git a/sdk/package/lib/inits/setupUninstall.ts b/sdk/package/lib/inits/setupUninstall.ts index c169457ec..fc4a71b8e 100644 --- a/sdk/package/lib/inits/setupUninstall.ts +++ b/sdk/package/lib/inits/setupUninstall.ts @@ -1,11 +1,11 @@ import * as T from "../../../base/lib/types" -export type UninstallFn = (opts: { +export type UninstallFn = (opts: { effects: T.Effects -}) => Promise -export class Uninstall { +}) => Promise +export class Uninstall { private constructor(readonly fn: UninstallFn) {} - static of( + static of( fn: UninstallFn, ) { return new Uninstall(fn) @@ -22,7 +22,7 @@ export class Uninstall { } } -export function setupUninstall( +export function setupUninstall( fn: UninstallFn, ) { return Uninstall.of(fn) diff --git a/sdk/package/lib/mainFn/CommandController.ts b/sdk/package/lib/mainFn/CommandController.ts index 601b759e6..27089ea86 100644 --- a/sdk/package/lib/mainFn/CommandController.ts +++ b/sdk/package/lib/mainFn/CommandController.ts @@ -20,7 +20,7 @@ export class CommandController { private process: cp.ChildProcessWithoutNullStreams, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) {} - static of() { + static of() { return async ( effects: T.Effects, subcontainer: diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts index bde4654ee..7877dce08 100644 --- a/sdk/package/lib/mainFn/Daemon.ts +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -17,7 +17,7 @@ export class Daemon { get subContainerHandle(): undefined | ExecSpawnable { return this.commandController?.subContainerHandle } - static of() { + static of() { return async ( effects: T.Effects, subcontainer: diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts index 4c2506d1d..4ad573b0b 100644 --- a/sdk/package/lib/mainFn/Daemons.ts +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -27,7 +27,7 @@ export type Ready = { } type DaemonsParams< - Manifest extends T.Manifest, + Manifest extends T.SDKManifest, Ids extends string, Command extends string, Id extends string, @@ -43,7 +43,7 @@ type DaemonsParams< type ErrorDuplicateId = `The id '${Id}' is already used` -export const runCommand = () => +export const runCommand = () => CommandController.of() /** @@ -69,7 +69,7 @@ Daemons.of({ }) ``` */ -export class Daemons +export class Daemons implements T.DaemonBuildable { private constructor( @@ -89,7 +89,7 @@ export class Daemons * @param options * @returns */ - static of(options: { + static of(options: { effects: T.Effects started: (onTerm: () => PromiseLike) => PromiseLike healthReceipts: HealthReceipt[] diff --git a/sdk/package/lib/mainFn/Mounts.ts b/sdk/package/lib/mainFn/Mounts.ts index 42d6d66ab..38b3ce2a7 100644 --- a/sdk/package/lib/mainFn/Mounts.ts +++ b/sdk/package/lib/mainFn/Mounts.ts @@ -3,7 +3,7 @@ import { MountOptions } from "../util/SubContainer" type MountArray = { path: string; options: MountOptions }[] -export class Mounts { +export class Mounts { private constructor( readonly volumes: { id: Manifest["volumes"][number] @@ -25,7 +25,7 @@ export class Mounts { }[], ) {} - static of() { + static of() { return new Mounts([], [], []) } @@ -57,7 +57,7 @@ export class Mounts { return this } - addDependency( + addDependency( dependencyId: keyof Manifest["dependencies"] & string, volumeId: DependencyManifest["volumes"][number], subpath: string | null, diff --git a/sdk/package/lib/mainFn/index.ts b/sdk/package/lib/mainFn/index.ts index f1373c44e..be30c652d 100644 --- a/sdk/package/lib/mainFn/index.ts +++ b/sdk/package/lib/mainFn/index.ts @@ -14,7 +14,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000 * @param fn * @returns */ -export const setupMain = ( +export const setupMain = ( fn: (o: { effects: T.Effects started(onTerm: () => PromiseLike): PromiseLike diff --git a/sdk/package/lib/manifest/setupManifest.ts b/sdk/package/lib/manifest/setupManifest.ts index 088575c1a..eb83741c8 100644 --- a/sdk/package/lib/manifest/setupManifest.ts +++ b/sdk/package/lib/manifest/setupManifest.ts @@ -14,6 +14,23 @@ import { VersionGraph } from "../version/VersionGraph" * @param manifest Static properties of the package */ export function setupManifest< + Id extends string, + Dependencies extends Record, + VolumesTypes extends VolumeId, + AssetTypes extends VolumeId, + ImagesTypes extends ImageId, + Manifest extends { + dependencies: Dependencies + id: Id + assets: AssetTypes[] + images: Record + volumes: VolumesTypes[] + }, +>(manifest: SDKManifest & Manifest): SDKManifest & Manifest { + return manifest +} + +export function buildManifest< Id extends string, Version extends string, Dependencies extends Record, @@ -27,7 +44,6 @@ export function setupManifest< images: Record volumes: VolumesTypes[] }, - Satisfies extends string[] = [], >( versions: VersionGraph, manifest: SDKManifest & Manifest, diff --git a/sdk/package/lib/test/inputSpecBuilder.test.ts b/sdk/package/lib/test/inputSpecBuilder.test.ts index bb5a8e25d..f9d4321a6 100644 --- a/sdk/package/lib/test/inputSpecBuilder.test.ts +++ b/sdk/package/lib/test/inputSpecBuilder.test.ts @@ -367,48 +367,39 @@ describe("values", () => { test("datetime", async () => { const sdk = StartSdk.of() .withManifest( - setupManifest( - VersionGraph.of( - VersionInfo.of({ - version: "1.0.0:0", - releaseNotes: "", - migrations: {}, - }), - ), - { - id: "testOutput", - title: "", - license: "", - wrapperRepo: "", - upstreamRepo: "", - supportSite: "", - marketingSite: "", - donationUrl: null, - description: { - short: "", - long: "", - }, - containers: {}, - images: {}, - volumes: [], - assets: [], - alerts: { - install: null, - update: null, - uninstall: null, - restore: null, - start: null, - stop: null, - }, - dependencies: { - "remote-test": { - description: "", - optional: true, - s9pk: "https://example.com/remote-test.s9pk", - }, + setupManifest({ + id: "testOutput", + title: "", + license: "", + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + containers: {}, + images: {}, + volumes: [], + assets: [], + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + "remote-test": { + description: "", + optional: true, + s9pk: "https://example.com/remote-test.s9pk", }, }, - ), + }), ) .withStore<{ test: "a" }>() .build(true) diff --git a/sdk/package/lib/test/output.sdk.ts b/sdk/package/lib/test/output.sdk.ts index 4cdf85111..d87446585 100644 --- a/sdk/package/lib/test/output.sdk.ts +++ b/sdk/package/lib/test/output.sdk.ts @@ -6,51 +6,40 @@ import { VersionGraph } from "../version/VersionGraph" export type Manifest = any export const sdk = StartSdk.of() .withManifest( - setupManifest( - VersionGraph.of( - VersionInfo.of({ - version: "1.0.0:0", - releaseNotes: "", - migrations: {}, - }) - .satisfies("#other:1.0.0:0") - .satisfies("#other:2.0.0:0"), - ), - { - id: "testOutput", - title: "", - license: "", - replaces: [], - wrapperRepo: "", - upstreamRepo: "", - supportSite: "", - marketingSite: "", - donationUrl: null, - description: { - short: "", - long: "", - }, - containers: {}, - images: {}, - volumes: [], - assets: [], - alerts: { - install: null, - update: null, - uninstall: null, - restore: null, - start: null, - stop: null, - }, - dependencies: { - "remote-test": { - description: "", - optional: false, - s9pk: "https://example.com/remote-test.s9pk", - }, + setupManifest({ + id: "testOutput", + title: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + containers: {}, + images: {}, + volumes: [], + assets: [], + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + "remote-test": { + description: "", + optional: false, + s9pk: "https://example.com/remote-test.s9pk", }, }, - ), + }), ) .withStore<{ storeRoot: { storeLeaf: "value" } }>() .build(true) diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index bb7190fe1..1a3be3e4f 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -86,19 +86,21 @@ export class FileHelper { /** * Accepts structured data and overwrites the existing file on disk. */ - async write(data: A) { + async write(data: A): Promise { const parent = previousPath.exec(this.path) if (parent) { await fs.mkdir(parent[1], { recursive: true }) } await fs.writeFile(this.path, this.writeData(data)) + + return null } /** * Reads the file from disk and converts it to structured data. */ - async read() { + private async readOnce(): Promise { if (!(await exists(this.path))) { return null } @@ -107,14 +109,14 @@ export class FileHelper { ) } - async const(effects: T.Effects) { - const watch = this.watch() + private async readConst(effects: T.Effects): Promise { + const watch = this.readWatch() const res = await watch.next() watch.next().then(effects.constRetry) return res.value } - async *watch() { + private async *readWatch() { let res while (true) { if (await exists(this.path)) { @@ -123,12 +125,12 @@ export class FileHelper { persistent: false, signal: ctrl.signal, }) - res = await this.read() + res = await this.readOnce() const listen = Promise.resolve() .then(async () => { for await (const _ of watch) { ctrl.abort("finished") - return + return null } }) .catch((e) => console.error(asError(e))) @@ -139,13 +141,22 @@ export class FileHelper { await onCreated(this.path).catch((e) => console.error(asError(e))) } } + return null + } + + get read() { + return { + once: () => this.readOnce(), + const: (effects: T.Effects) => this.readConst(effects), + watch: () => this.readWatch(), + } } /** * Accepts structured data and performs a merge with the existing file on disk. */ async merge(data: A) { - const fileData = (await this.read().catch(() => ({}))) || {} + const fileData = (await this.readOnce().catch(() => ({}))) || {} const mergeData = merge({}, fileData, data) return await this.write(mergeData) } diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index b5c0fba5d..2294d800b 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha8", + "version": "0.3.6-alpha9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha8", + "version": "0.3.6-alpha9", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -14,7 +14,7 @@ "@noble/hashes": "^1.4.0", "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", - "mime": "^4.0.3", + "mime-types": "^2.1.35", "ts-matches": "^5.5.1", "yaml": "^2.2.2" }, @@ -3136,18 +3136,25 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz", - "integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==", - "funding": [ - "https://github.com/sponsors/broofa" - ], - "bin": { - "mime": "bin/cli.js" + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" }, "engines": { - "node": ">=16" + "node": ">= 0.6" } }, "node_modules/mimic-fn": { diff --git a/sdk/package/package.json b/sdk/package/package.json index 530a22053..acc523594 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha8", + "version": "0.3.6-alpha.12", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", @@ -15,7 +15,7 @@ }, "scripts": { "test": "jest -c ./jest.config.js --coverage", - "buildOutput": "ts-node ./lib/test/makeOutput.ts && npx prettier --write '**/*.ts'", + "buildOutput": "ts-node ./lib/test/makeOutput.ts && npx prettier --write \"**/*.ts\"", "check": "tsc --noEmit", "tsc": "tsc" }, @@ -32,7 +32,7 @@ "dependencies": { "isomorphic-fetch": "^3.0.0", "lodash.merge": "^4.6.2", - "mime": "^4.0.3", + "mime-types": "^2.1.35", "ts-matches": "^5.5.1", "yaml": "^2.2.2", "@iarna/toml": "^2.2.5", diff --git a/web/README.md b/web/README.md index 7665a26a6..feddf713e 100644 --- a/web/README.md +++ b/web/README.md @@ -48,6 +48,8 @@ npm i npm run build:deps ``` +> Note if you are on **Windows** you need to install `make` for these scripts to work. Easiest way to do so is to install [Chocolatey](https://chocolatey.org/install) and then run `choco install make`. + #### Copy `config-sample.json` to a new file `config.json`. ```sh diff --git a/web/ionic.config.json b/web/ionic.config.json index ee434f78a..c5810bc10 100644 --- a/web/ionic.config.json +++ b/web/ionic.config.json @@ -17,12 +17,6 @@ "integrations": {}, "type": "angular", "root": "projects/setup-wizard" - }, - "diagnostic-ui": { - "name": "diagnostic-ui", - "integrations": {}, - "type": "angular", - "root": "projects/diagnostic-ui" } }, "defaultProject": "ui" diff --git a/web/lint-staged.config.js b/web/lint-staged.config.js index 80ea7cf8b..731cc9d5e 100644 --- a/web/lint-staged.config.js +++ b/web/lint-staged.config.js @@ -4,7 +4,6 @@ module.exports = { 'projects/ui/**/*.ts': () => 'npm run check:ui', 'projects/shared/**/*.ts': () => 'npm run check:shared', 'projects/marketplace/**/*.ts': () => 'npm run check:marketplace', - 'projects/diagnostic-ui/**/*.ts': () => 'npm run check:dui', 'projects/install-wizard/**/*.ts': () => 'npm run check:install-wiz', 'projects/setup-wizard/**/*.ts': () => 'npm run check:setup', } diff --git a/web/package.json b/web/package.json index 0de550c62..5fdf2c859 100644 --- a/web/package.json +++ b/web/package.json @@ -6,14 +6,13 @@ "license": "MIT", "scripts": { "ng": "ng", - "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup && npm run check:dui", + "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup", "check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck", "check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck", - "check:dui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck", "check:install-wiz": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck", "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", - "build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)", + "build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)", "build:install-wiz": "ng run install-wizard:build", "build:setup": "ng run setup-wizard:build", "build:ui": "ng run ui:build", diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.ts b/web/projects/setup-wizard/src/app/pages/success/success.page.ts index dab0b44a6..aa0c7fc90 100644 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -83,7 +83,7 @@ export class SuccessPage { await this.api.exit() } } catch (e: any) { - await this.errorService.handleError(e) + this.errorService.handleError(e) } } diff --git a/web/projects/shared/src/types/workspace-config.ts b/web/projects/shared/src/types/workspace-config.ts index 57d5e2a4c..d10c4b07e 100644 --- a/web/projects/shared/src/types/workspace-config.ts +++ b/web/projects/shared/src/types/workspace-config.ts @@ -2,7 +2,7 @@ export type WorkspaceConfig = { gitHash: string useMocks: boolean enableWidgets: boolean - // each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard, diagnostic-ui + // each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard ui: { api: { url: string diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.ts b/web/projects/ui/src/app/app/preloader/preloader.component.ts index 177362222..70c5e2995 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.component.ts @@ -65,7 +65,6 @@ const ICONS = [ 'options-outline', 'pencil', 'phone-portrait-outline', - 'play-circle-outline', 'play-outline', 'power', 'pricetag-outline', diff --git a/web/projects/ui/src/app/components/form.component.ts b/web/projects/ui/src/app/components/form.component.ts index 386bd5f8e..93f65115a 100644 --- a/web/projects/ui/src/app/components/form.component.ts +++ b/web/projects/ui/src/app/components/form.component.ts @@ -149,14 +149,15 @@ export class FormComponent> implements OnInit { } private process(operations: Operation[]) { - operations.forEach(({ op, path }) => { - const control = this.form.get(path.substring(1).split('/')) + operations.forEach(operation => { + const control = this.form.get(operation.path.substring(1).split('/')) if (!control || !control.parent) return - if (op !== 'remove') { + if (operation.op === 'add' || operation.op === 'replace') { control.markAsDirty() control.markAsTouched() + control.setValue(operation.value) } control.parent.markAsDirty() diff --git a/web/projects/ui/src/app/modals/action-input.component.ts b/web/projects/ui/src/app/modals/action-input.component.ts index 494dfcdda..4543e29fd 100644 --- a/web/projects/ui/src/app/modals/action-input.component.ts +++ b/web/projects/ui/src/app/modals/action-input.component.ts @@ -16,7 +16,7 @@ import { compare } from 'fast-json-patch' import { PatchDB } from 'patch-db-client' import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs' import { InvalidService } from 'src/app/components/form/invalid.service' -import { ActionDepComponent } from 'src/app/modals/action-dep.component' +import { ActionRequestInfoComponent } from 'src/app/modals/action-request-input.component' import { UiPipeModule } from 'src/app/pipes/ui/ui.module' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -25,23 +25,29 @@ import * as json from 'fast-json-patch' import { ActionService } from '../services/action.service' import { ActionButton, FormComponent } from '../components/form.component' -export interface PackageActionData { - readonly pkgInfo: { +export type PackageActionData = { + pkgInfo: { id: string title: string + icon: string + mainStatus: T.MainStatus['main'] } - readonly actionInfo: { + actionInfo: { id: string - warning: string | null + metadata: T.ActionMetadata } - readonly dependentInfo?: { - title: string + requestInfo?: { + dependentId?: string request: T.ActionRequest } } @Component({ template: ` +
+ +

{{ pkgInfo.title }}

+
@@ -52,13 +58,11 @@ export interface PackageActionData {
- + > [] = [ { @@ -131,12 +147,12 @@ export class ActionInputModal { return { spec: res.spec, originalValue, - operations: this.dependentInfo?.request.input + operations: this.requestInfo?.request.input ? compare( - originalValue, + JSON.parse(JSON.stringify(originalValue)), utils.deepMerge( - originalValue, - this.dependentInfo.request.input.value, + JSON.parse(JSON.stringify(originalValue)), + this.requestInfo.request.input.value, ) as object, ) : null, @@ -159,15 +175,7 @@ export class ActionInputModal { async execute(input: object) { if (await this.checkConflicts(input)) { - const res = await firstValueFrom(this.res$) - - return this.actionService.execute(this.pkgInfo.id, this.actionId, { - prev: { - spec: res.spec, - value: res.originalValue, - }, - curr: input, - }) + return this.actionService.execute(this.pkgInfo.id, this.actionId, input) } } @@ -181,6 +189,7 @@ export class ActionInputModal { Object.values(packages[id].requestedActions).some( ({ request, active }) => !active && + request.severity === 'critical' && request.packageId === this.pkgInfo.id && request.actionId === this.actionId && request.when?.condition === 'input-not-matches' && diff --git a/web/projects/ui/src/app/modals/action-dep.component.ts b/web/projects/ui/src/app/modals/action-request-input.component.ts similarity index 77% rename from web/projects/ui/src/app/modals/action-dep.component.ts rename to web/projects/ui/src/app/modals/action-request-input.component.ts index 8eeb9faa8..2451de876 100644 --- a/web/projects/ui/src/app/modals/action-dep.component.ts +++ b/web/projects/ui/src/app/modals/action-request-input.component.ts @@ -11,14 +11,10 @@ import { CommonModule } from '@angular/common' import { TuiNotificationModule } from '@taiga-ui/core' @Component({ - selector: 'action-dep', + selector: 'action-request-info', template: ` - -

- {{ pkgTitle }} -

- The following modifications have been made to {{ pkgTitle }} to satisfy - {{ depTitle }}: + + The following modifications were made:
@@ -27,14 +23,15 @@ import { TuiNotificationModule } from '@taiga-ui/core' standalone: true, imports: [CommonModule, TuiNotificationModule], changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + tui-notification { + margin-bottom: 1.5rem; + } + `, + ], }) -export class ActionDepComponent implements OnInit { - @Input() - pkgTitle = '' - - @Input() - depTitle = '' - +export class ActionRequestInfoComponent implements OnInit { @Input() originalValue: object = {} @@ -68,15 +65,15 @@ export class ActionDepComponent implements OnInit { private getMessage(operation: Operation): string { switch (operation.op) { case 'add': - return `Added ${this.getNewValue(operation.value)}` + return `added ${this.getNewValue(operation.value)}` case 'remove': - return `Removed ${this.getOldValue(operation.path)}` + return `removed ${this.getOldValue(operation.path)}` case 'replace': - return `Changed from ${this.getOldValue( + return `changed from ${this.getOldValue( operation.path, )} to ${this.getNewValue(operation.value)}` default: - return `Unknown operation` + return `Unknown operation` // unreachable } } diff --git a/web/projects/ui/src/app/modals/action-success/action-success-group.component.ts b/web/projects/ui/src/app/modals/action-success/action-success-group.component.ts new file mode 100644 index 000000000..c02b51ee9 --- /dev/null +++ b/web/projects/ui/src/app/modals/action-success/action-success-group.component.ts @@ -0,0 +1,54 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { TuiFadeModule, TuiTitleModule } from '@taiga-ui/experimental' +import { TuiAccordionModule } from '@taiga-ui/kit' +import { ActionSuccessItemComponent } from './action-success-item.component' + +@Component({ + standalone: true, + selector: 'app-action-success-group', + template: ` +

+ + +

{{ item.name }}
+ + + + +

+ `, + styles: [ + ` + p:first-child { + margin-top: 0; + } + + p:last-child { + margin-bottom: 0; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + TuiTitleModule, + ActionSuccessItemComponent, + TuiAccordionModule, + TuiFadeModule, + ], +}) +export class ActionSuccessGroupComponent { + @Input() + value?: T.ActionResultV1 & { type: 'object' } + + isSingle( + value: T.ActionResultV1, + ): value is T.ActionResultV1 & { type: 'string' } { + return value.type === 'string' + } +} diff --git a/web/projects/ui/src/app/modals/action-success/action-success-item.component.ts b/web/projects/ui/src/app/modals/action-success/action-success-item.component.ts new file mode 100644 index 000000000..25054f2f6 --- /dev/null +++ b/web/projects/ui/src/app/modals/action-success/action-success-item.component.ts @@ -0,0 +1,184 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + ElementRef, + inject, + Input, + TemplateRef, + ViewChild, +} from '@angular/core' +import { FormsModule } from '@angular/forms' +import { T } from '@start9labs/start-sdk' +import { + TuiDialogService, + TuiLabelModule, + TuiTextfieldComponent, + TuiTextfieldControllerModule, +} from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { TuiInputModule } from '@taiga-ui/kit' +import { QrCodeModule } from 'ng-qrcode' +import { ActionSuccessGroupComponent } from './action-success-group.component' + +@Component({ + standalone: true, + selector: 'app-action-success-item', + template: ` +

+ +

+ + + + + + + + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [ + ` + @import '@taiga-ui/core/styles/taiga-ui-local'; + + .reveal { + @include center-all(); + } + + .qr { + position: relative; + text-align: center; + } + `, + ], + imports: [ + CommonModule, + FormsModule, + TuiInputModule, + TuiTextfieldControllerModule, + TuiButtonModule, + QrCodeModule, + TuiLabelModule, + ], +}) +export class ActionSuccessItemComponent { + @ViewChild(TuiTextfieldComponent, { read: ElementRef }) + private readonly input!: ElementRef + private readonly dialogs = inject(TuiDialogService) + + readonly parent = inject(ActionSuccessGroupComponent, { + optional: true, + }) + + @Input() + value!: T.ActionResultV1 & { type: 'string' } + + masked = true + + get border(): number { + let border = 0 + + if (this.value.masked) { + border += 2 + } + + if (this.value.copyable) { + border += 2 + } + + if (this.value.qr && this.parent) { + border += 2 + } + + return border + } + + show(template: TemplateRef) { + const masked = this.masked + + this.masked = this.value.masked + this.dialogs + .open(template, { label: 'Scan this QR', size: 's' }) + .subscribe({ + complete: () => (this.masked = masked), + }) + } + + copy() { + const el = this.input.nativeElement + + if (!el) { + return + } + + el.type = 'text' + el.focus() + el.select() + el.ownerDocument.execCommand('copy') + el.type = this.masked && this.value.masked ? 'password' : 'text' + } +} diff --git a/web/projects/ui/src/app/modals/action-success/action-success.module.ts b/web/projects/ui/src/app/modals/action-success/action-success.module.ts deleted file mode 100644 index 23c123081..000000000 --- a/web/projects/ui/src/app/modals/action-success/action-success.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { ActionSuccessPage } from './action-success.page' -import { QrCodeModule } from 'ng-qrcode' - -@NgModule({ - declarations: [ActionSuccessPage], - imports: [CommonModule, IonicModule, QrCodeModule], - exports: [ActionSuccessPage], -}) -export class ActionSuccessPageModule {} diff --git a/web/projects/ui/src/app/modals/action-success/action-success.page.html b/web/projects/ui/src/app/modals/action-success/action-success.page.html deleted file mode 100644 index da8cc7be5..000000000 --- a/web/projects/ui/src/app/modals/action-success/action-success.page.html +++ /dev/null @@ -1,35 +0,0 @@ - - - Execution Complete - - - - - - - - - -

{{ actionRes.message }}

- -
- diff --git a/web/projects/ui/src/app/modals/action-success/action-success.page.scss b/web/projects/ui/src/app/modals/action-success/action-success.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/modals/action-success/action-success.page.ts b/web/projects/ui/src/app/modals/action-success/action-success.page.ts index 8912ed6ba..afcab5793 100644 --- a/web/projects/ui/src/app/modals/action-success/action-success.page.ts +++ b/web/projects/ui/src/app/modals/action-success/action-success.page.ts @@ -1,39 +1,36 @@ -import { Component, Input } from '@angular/core' -import { ModalController, ToastController } from '@ionic/angular' -import { copyToClipboard } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' +import { CommonModule } from '@angular/common' +import { Component, inject } from '@angular/core' +import { TuiDialogContext, TuiTextfieldControllerModule } from '@taiga-ui/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { RR } from 'src/app/services/api/api.types' +import { ActionSuccessGroupComponent } from './action-success-group.component' +import { ActionSuccessItemComponent } from './action-success-item.component' @Component({ - selector: 'action-success', - templateUrl: './action-success.page.html', - styleUrls: ['./action-success.page.scss'], + standalone: true, + template: ` + + + + + `, + imports: [ + CommonModule, + ActionSuccessGroupComponent, + ActionSuccessItemComponent, + TuiTextfieldControllerModule, + ], }) export class ActionSuccessPage { - @Input() - actionRes!: T.ActionResult + readonly data = + inject>(POLYMORPHEUS_CONTEXT).data - constructor( - private readonly modalCtrl: ModalController, - private readonly toastCtrl: ToastController, - ) {} - - async copy(address: string) { - let message = '' - await copyToClipboard(address || '').then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async dismiss() { - return this.modalCtrl.dismiss() - } + readonly item = this.data?.type === 'string' ? this.data : null + readonly group = this.data?.type === 'object' ? this.data : null } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts index 84e52d15f..377504238 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts @@ -5,7 +5,6 @@ import { IonicModule } from '@ionic/angular' import { AppActionsPage, AppActionsItemComponent } from './app-actions.page' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { SharedPipesModule } from '@start9labs/shared' -import { ActionSuccessPageModule } from 'src/app/modals/action-success/action-success.module' const routes: Routes = [ { @@ -21,7 +20,6 @@ const routes: Routes = [ RouterModule.forChild(routes), QRComponentModule, SharedPipesModule, - ActionSuccessPageModule, ], declarations: [AppActionsPage, AppActionsItemComponent], }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html index c9ecc8ed5..bacbed729 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.html @@ -37,8 +37,8 @@ diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 95be585ba..feea649d1 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -21,13 +21,12 @@ export class AppActionsPage { filter(pkg => pkg.stateInfo.state === 'installed'), map(pkg => ({ mainStatus: pkg.status.main, + icon: pkg.icon, manifest: getManifest(pkg), - actions: Object.keys(pkg.actions) - .filter(id => id !== 'config') - .map(id => ({ - id, - ...pkg.actions[id], - })), + actions: Object.keys(pkg.actions).map(id => ({ + id, + ...pkg.actions[id], + })), })), ) @@ -40,13 +39,14 @@ export class AppActionsPage { async handleAction( mainStatus: T.MainStatus['main'], + icon: string, manifest: T.Manifest, action: T.ActionMetadata & { id: string }, ) { - this.actionService.present( - { id: manifest.id, title: manifest.title, mainStatus }, - { id: action.id, metadata: action }, - ) + this.actionService.present({ + pkgInfo: { id: manifest.id, title: manifest.title, icon, mainStatus }, + actionInfo: { id: action.id, metadata: action }, + }) } async rebuild(id: string) { @@ -76,7 +76,7 @@ export class AppActionsItemComponent { get disabledText() { return ( typeof this.action.visibility === 'object' && - this.action.visibility.disabled.reason + this.action.visibility.disabled ) } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts deleted file mode 100644 index 2c53d0fea..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' -import { IonicModule } from '@ionic/angular' -import { AppMetricsPage } from './app-metrics.page' -import { SharedPipesModule } from '@start9labs/shared' -import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module' - -const routes: Routes = [ - { - path: '', - component: AppMetricsPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - SharedPipesModule, - SkeletonListComponentModule, - ], - declarations: [AppMetricsPage], -}) -export class AppMetricsPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html deleted file mode 100644 index cca899f46..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Monitor - - - - - - - - - - - {{ metric.key }} - - - {{ metric.value.value }} {{ metric.value.unit }} - - - - - diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss deleted file mode 100644 index eea898305..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -.metric-note { - font-size: 16px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts deleted file mode 100644 index 61b17b669..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ErrorService, getPkgId, pauseFor } from '@start9labs/shared' -import { Metric } from 'src/app/services/api/api.types' -import { ApiService } from 'src/app/services/api/embassy-api.service' - -@Component({ - selector: 'app-metrics', - templateUrl: './app-metrics.page.html', - styleUrls: ['./app-metrics.page.scss'], -}) -export class AppMetricsPage { - loading = true - readonly pkgId = getPkgId(this.route) - going = false - metrics?: Metric - - constructor( - private readonly route: ActivatedRoute, - private readonly errorService: ErrorService, - private readonly embassyApi: ApiService, - ) {} - - ngOnInit() { - this.startDaemon() - } - - ngOnDestroy() { - this.stopDaemon() - } - - async startDaemon(): Promise { - this.going = true - while (this.going) { - const startTime = Date.now() - await this.getMetrics() - await pauseFor(Math.max(4000 - (Date.now() - startTime), 0)) - } - } - - stopDaemon() { - this.going = false - } - - async getMetrics(): Promise { - try { - this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId }) - } catch (e: any) { - this.errorService.handleError(e) - this.stopDaemon() - } finally { - this.loading = false - } - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts deleted file mode 100644 index 2d8553017..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' -import { IonicModule } from '@ionic/angular' -import { AppPropertiesPage } from './app-properties.page' -import { QRComponentModule } from 'src/app/components/qr/qr.component.module' -import { MaskPipeModule } from 'src/app/pipes/mask/mask.module' -import { - SharedPipesModule, - TextSpinnerComponentModule, -} from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: AppPropertiesPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - QRComponentModule, - SharedPipesModule, - TextSpinnerComponentModule, - MaskPipeModule, - ], - declarations: [AppPropertiesPage], -}) -export class AppPropertiesPageModule {} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html deleted file mode 100644 index ca3cdd3be..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.html +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - Properties - - - - Refresh - - - - - - - - - - - - -

- - Service is stopped. Information on this page could be inaccurate. - -

-
-
- - - - -

No properties.

-
-
- - - -
- - - - - - -

{{ prop.key }}

-
-
- - - - - - -

{{ prop.key }}

-

- {{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value | - mask : 64) : prop.value.value }} -

-
-
- - - - - - - - - -
-
-
-
-
-
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss deleted file mode 100644 index eea898305..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -.metric-note { - font-size: 16px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts deleted file mode 100644 index 521542c10..000000000 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Component, ViewChild } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { - AlertController, - IonBackButtonDelegate, - ModalController, - NavController, - ToastController, -} from '@ionic/angular' -import { copyToClipboard, ErrorService, getPkgId } from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' -import { getValueByPointer } from 'fast-json-patch' -import { PatchDB } from 'patch-db-client' -import { map, takeUntil } from 'rxjs/operators' -import { QRComponent } from 'src/app/components/qr/qr.component' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { PackageProperties } from 'src/app/util/properties.util' - -@Component({ - selector: 'app-properties', - templateUrl: './app-properties.page.html', - styleUrls: ['./app-properties.page.scss'], - providers: [TuiDestroyService], -}) -export class AppPropertiesPage { - loading = true - readonly pkgId = getPkgId(this.route) - - pointer = '' - node: PackageProperties = {} - - properties: PackageProperties = {} - unmasked: { [key: string]: boolean } = {} - - stopped$ = this.patch - .watch$('packageData', this.pkgId, 'status', 'main') - .pipe(map(status => status === 'stopped')) - - @ViewChild(IonBackButtonDelegate, { static: false }) - backButton?: IonBackButtonDelegate - - constructor( - private readonly route: ActivatedRoute, - private readonly embassyApi: ApiService, - private readonly errorService: ErrorService, - private readonly alertCtrl: AlertController, - private readonly toastCtrl: ToastController, - private readonly modalCtrl: ModalController, - private readonly navCtrl: NavController, - private readonly patch: PatchDB, - private readonly destroy$: TuiDestroyService, - ) {} - - ionViewDidEnter() { - if (!this.backButton) return - this.backButton.onClick = () => { - history.back() - } - } - - async ngOnInit() { - await this.getProperties() - - this.route.queryParams - .pipe(takeUntil(this.destroy$)) - .subscribe(queryParams => { - if (queryParams['pointer'] === this.pointer) return - this.pointer = queryParams['pointer'] || '' - this.node = getValueByPointer(this.properties, this.pointer) - }) - } - - async refresh() { - await this.getProperties() - } - - async presentDescription( - property: { key: string; value: PackageProperties[''] }, - e: Event, - ) { - e.stopPropagation() - - const alert = await this.alertCtrl.create({ - header: property.key, - message: property.value.description || undefined, - }) - await alert.present() - } - - async goToNested(key: string): Promise { - this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, { - queryParams: { - pointer: `${this.pointer}/${key}/value`, - }, - }) - } - - async copy(text: string): Promise { - let message = '' - await copyToClipboard(text).then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async showQR(text: string): Promise { - const modal = await this.modalCtrl.create({ - component: QRComponent, - componentProps: { - text, - }, - cssClass: 'qr-modal', - }) - await modal.present() - } - - toggleMask(key: string) { - this.unmasked[key] = !this.unmasked[key] - } - - private async getProperties(): Promise { - this.loading = true - try { - this.properties = await this.embassyApi.getPackageProperties({ - id: this.pkgId, - }) - this.node = getValueByPointer(this.properties, this.pointer) - } catch (e: any) { - this.errorService.handleError(e) - } finally { - this.loading = false - } - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 7d51a6fc8..5787948d6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -19,6 +19,7 @@ import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.c import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component' import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component' import { AppShowErrorComponent } from './components/app-show-error/app-show-error.component' +import { AppShowActionRequestsComponent } from './components/app-show-action-requests/app-show-action-requests.component' import { HealthColorPipe } from './pipes/health-color.pipe' import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe' import { ToButtonsPipe } from './pipes/to-buttons.pipe' @@ -45,6 +46,7 @@ const routes: Routes = [ AppShowHealthChecksComponent, AppShowAdditionalComponent, AppShowErrorComponent, + AppShowActionRequestsComponent, ], imports: [ CommonModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index ec59b1042..286f3ef5b 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -20,6 +20,12 @@ + + any } @@ -60,6 +59,7 @@ export class AppShowPage { const pkg = allPkgs[this.pkgId] const manifest = getManifest(pkg) return { + allPkgs, pkg, manifest, dependencies: this.getDepInfo(pkg, manifest, allPkgs, depErrors), @@ -75,7 +75,6 @@ export class AppShowPage { private readonly navCtrl: NavController, private readonly patch: PatchDB, private readonly depErrorService: DepErrorService, - private readonly actionService: ActionService, ) {} showProgress( @@ -95,32 +94,6 @@ export class AppShowPage { ) } - private getDepDetails( - pkg: PackageDataEntry, - allPkgs: AllPackageData, - depId: string, - ) { - const { title, icon, versionRange } = pkg.currentDependencies[depId] - - if ( - allPkgs[depId] && - (allPkgs[depId].stateInfo.state === 'installed' || - allPkgs[depId].stateInfo.state === 'updating') - ) { - return { - title: allPkgs[depId].stateInfo.manifest!.title, - icon: allPkgs[depId].icon, - versionRange, - } - } else { - return { - title: title || depId, - icon: icon || 'assets/img/service-icons/fallback.png', - versionRange, - } - } - } - private getDepValues( pkg: PackageDataEntry, allPkgs: AllPackageData, @@ -135,23 +108,16 @@ export class AppShowPage { depErrors, ) - const { title, icon, versionRange } = this.getDepDetails( - pkg, - allPkgs, - depId, - ) + const { title, icon, versionRange } = getDepDetails(pkg, allPkgs, depId) return { id: depId, version: versionRange, title, icon, - errorText: errorText - ? `${errorText}. ${manifest.title} will not work as expected.` - : '', - actionText: fixText || 'View', - action: - fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)), + errorText: errorText ? errorText : '', + actionText: fixText, + action: fixAction, } } @@ -165,28 +131,31 @@ export class AppShowPage { let errorText: string | null = null let fixText: string | null = null - let fixAction: (() => any) | null = null + let fixAction: () => any = () => {} if (depError) { if (depError.type === 'notInstalled') { errorText = 'Not installed' fixText = 'Install' - fixAction = () => this.fixDep(pkg, manifest, 'install', depId) + fixAction = () => this.installDep(pkg, manifest, depId) } else if (depError.type === 'incorrectVersion') { errorText = 'Incorrect version' fixText = 'Update' - fixAction = () => this.fixDep(pkg, manifest, 'update', depId) - } else if (depError.type === 'configUnsatisfied') { - errorText = 'Config not satisfied' - fixText = 'Auto config' - fixAction = () => this.fixDep(pkg, manifest, 'configure', depId) + fixAction = () => this.installDep(pkg, manifest, depId) + } else if (depError.type === 'actionRequired') { + errorText = 'Action Required (see above)' } else if (depError.type === 'notRunning') { errorText = 'Not running' fixText = 'Start' + fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`) } else if (depError.type === 'healthChecksFailed') { errorText = 'Required health check not passing' + fixText = 'View' + fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`) } else if (depError.type === 'transitive') { errorText = 'Dependency has a dependency issue' + fixText = 'View' + fixAction = () => this.navCtrl.navigateForward(`/services/${depId}`) } } @@ -197,41 +166,6 @@ export class AppShowPage { } } - private async fixDep( - pkg: PackageDataEntry, - pkgManifest: T.Manifest, - action: 'install' | 'update' | 'configure', - depId: string, - ) { - switch (action) { - case 'install': - case 'update': - return this.installDep(pkg, pkgManifest, depId) - case 'configure': - const depPkg = await getPackage(this.patch, depId) - if (!depPkg) return - - const depManifest = getManifest(depPkg) - return this.actionService.present( - { - id: depId, - title: depManifest.title, - mainStatus: depPkg.status.main, - }, - { id: 'config', metadata: pkg.actions['config'] }, - { - title: pkgManifest.title, - request: Object.values(pkg.requestedActions).find( - r => - r.active && - r.request.packageId === depId && - r.request.actionId === 'config', - )!.request, - }, - ) - } - } - private async installDep( pkg: PackageDataEntry, pkgManifest: T.Manifest, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html new file mode 100644 index 000000000..2aee18f05 --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.html @@ -0,0 +1,45 @@ + + Required Actions + + + +

{{ request.actionName }}

+

+ Service: + + {{ request.dependency.title }} +

+

+ Reason: + {{ request.reason || 'no reason provided' }} +

+
+
+
+ + + Requested Actions + + + +

{{ request.actionName }}

+

+ Service: + + {{ request.dependency.title }} +

+

+ Reason: + {{ request.reason || 'no reason provided' }} +

+
+
+
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss new file mode 100644 index 000000000..c83e6f6a7 --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.scss @@ -0,0 +1,16 @@ +.light { + color: var(--ion-color-dark); +} + +.highlighted { + color: var(--ion-color-dark); + font-weight: bold; +} + +.dependency { + display: inline-flex; + img { + max-width: 16px; + margin: 0 2px 0 5px; + } +} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts new file mode 100644 index 000000000..0fefe17be --- /dev/null +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-action-requests/app-show-action-requests.component.ts @@ -0,0 +1,94 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { T } from '@start9labs/start-sdk' +import { ActionService } from 'src/app/services/action.service' +import { getDepDetails } from 'src/app/util/dep-info' + +@Component({ + selector: 'app-show-action-requests', + templateUrl: './app-show-action-requests.component.html', + styleUrls: ['./app-show-action-requests.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppShowActionRequestsComponent { + @Input() + allPkgs!: Record + + @Input() + pkg!: T.PackageDataEntry + + @Input() + manifest!: T.Manifest + + get actionRequests() { + const critical: (T.ActionRequest & { + actionName: string + dependency: { + title: string + icon: string + } | null + })[] = [] + const important: (T.ActionRequest & { + actionName: string + dependency: { + title: string + icon: string + } | null + })[] = [] + + Object.values(this.pkg.requestedActions) + .filter(r => r.active) + .forEach(r => { + const self = r.request.packageId === this.manifest.id + const toReturn = { + ...r.request, + actionName: self + ? this.pkg.actions[r.request.actionId].name + : this.allPkgs[r.request.packageId]?.actions[r.request.actionId] + .name || 'Unknown Action', + dependency: self + ? null + : getDepDetails(this.pkg, this.allPkgs, r.request.packageId), + } + + if (r.request.severity === 'critical') { + critical.push(toReturn) + } else { + important.push(toReturn) + } + }) + + return { critical, important } + } + + constructor(private readonly actionService: ActionService) {} + + async handleAction(request: T.ActionRequest) { + const self = request.packageId === this.manifest.id + this.actionService.present({ + pkgInfo: { + id: request.packageId, + title: self + ? this.manifest.title + : getDepDetails(this.pkg, this.allPkgs, request.packageId).title, + mainStatus: self + ? this.pkg.status.main + : this.allPkgs[request.packageId].status.main, + icon: self + ? this.pkg.icon + : getDepDetails(this.pkg, this.allPkgs, request.packageId).icon, + }, + actionInfo: { + id: request.actionId, + metadata: + request.packageId === this.manifest.id + ? this.pkg.actions[request.actionId] + : this.allPkgs[request.packageId].actions[request.actionId], + }, + requestInfo: { + request, + dependentId: + request.packageId === this.manifest.id ? undefined : this.manifest.id, + }, + }) + } +} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html index b184b83ff..059dc208b 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html @@ -1,6 +1,10 @@ Dependencies - + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index e31d4cd24..ef8f9a44e 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -52,16 +52,6 @@ Start - - - Configure - - , - private readonly actionService: ActionService, ) {} get interfaces(): PackageDataEntry['serviceInterfaces'] { @@ -77,14 +73,11 @@ export class AppShowStatusComponent { } get canStart(): boolean { - return ( - this.status.primary === 'stopped' && - !Object.keys(this.pkg.requestedActions).length - ) + return this.status.primary === 'stopped' } get sigtermTimeout(): string | null { - return this.pkgStatus?.main === 'stopping' ? '30s' : null // @dr-bonez TODO + return this.pkgStatus?.main === 'stopping' ? '30s' : null // @TODO Aiden } launchUi( @@ -94,17 +87,6 @@ export class AppShowStatusComponent { this.launcherService.launch(interfaces, hosts) } - async presentModalConfig(): Promise { - return this.actionService.present( - { - id: this.manifest.id, - title: this.manifest.title, - mainStatus: this.pkg.status.main, - }, - { id: 'config', metadata: this.pkg.actions['config'] }, - ) - } - async tryStart(): Promise { if (this.status.dependency === 'warning') { const depErrMsg = `${this.manifest.title} has unmet dependencies. It will not work as expected.` @@ -221,6 +203,7 @@ export class AppShowStatusComponent { loader.unsubscribe() } } + private async presentAlertStart(message: string): Promise { return new Promise(async resolve => { const alert = await this.alertCtrl.create({ diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index c9bb73bfe..b2101ce29 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { AlertController, ModalController, NavController } from '@ionic/angular' +import { ModalController, NavController } from '@ionic/angular' import { MarkdownComponent } from '@start9labs/shared' import { DataModel, @@ -8,10 +8,8 @@ import { PackageDataEntry, } from 'src/app/services/patch-db/data-model' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { from, map, Observable, of } from 'rxjs' +import { from, map, Observable } from 'rxjs' import { PatchDB } from 'patch-db-client' -import { ActionService } from 'src/app/services/action.service' -import { needsConfig } from 'src/app/util/get-package-data' export interface Button { title: string @@ -33,8 +31,6 @@ export class ToButtonsPipe implements PipeTransform { private readonly apiService: ApiService, private readonly api: ApiService, private readonly patch: PatchDB, - private readonly actionService: ActionService, - private readonly alertCtrl: AlertController, ) {} transform(pkg: PackageDataEntry): Button[] { @@ -51,50 +47,12 @@ export class ToButtonsPipe implements PipeTransform { .watch$('ui', 'ackInstructions', manifest.id) .pipe(map(seen => !seen)), }, - // config - { - action: async () => - pkg.actions['config'] - ? this.actionService.present( - { - id: manifest.id, - title: manifest.title, - mainStatus: pkg.status.main, - }, - { - id: 'config', - metadata: pkg.actions['config'], - }, - ) - : this.alertCtrl - .create({ - header: 'No Config', - message: `No config options for ${manifest.title} v${manifest.version}`, - buttons: ['OK'], - }) - .then(a => a.present()), - title: 'Config', - description: `Customize ${manifest.title}`, - icon: 'options-outline', - highlighted$: of(needsConfig(manifest.id, pkg.requestedActions)), - }, - // properties - { - action: () => - this.navCtrl.navigateForward(['properties'], { - relativeTo: this.route, - }), - title: 'Properties', - description: - 'Runtime information, credentials, and other values of interest', - icon: 'briefcase-outline', - }, // actions { action: () => this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }), title: 'Actions', - description: `Uninstall and other commands specific to ${manifest.title}`, + description: `All actions for ${manifest.title}`, icon: 'flash-outline', }, // interfaces diff --git a/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts b/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts index 9dfbddcad..06f4b45fe 100644 --- a/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -36,20 +36,6 @@ const routes: Routes = [ loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), }, - { - path: ':pkgId/metrics', - loadChildren: () => - import('./app-metrics/app-metrics.module').then( - m => m.AppMetricsPageModule, - ), - }, - { - path: ':pkgId/properties', - loadChildren: () => - import('./app-properties/app-properties.module').then( - m => m.AppPropertiesPageModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/services/action.service.ts b/web/projects/ui/src/app/services/action.service.ts index 74df5ffc6..df09daa32 100644 --- a/web/projects/ui/src/app/services/action.service.ts +++ b/web/projects/ui/src/app/services/action.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core' -import { AlertController, ModalController } from '@ionic/angular' +import { AlertController } from '@ionic/angular' import { ErrorService, LoadingService } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' +import { TuiDialogService } from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' -import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { @@ -31,28 +31,16 @@ const allowedStatuses = { export class ActionService { constructor( private readonly api: ApiService, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly alertCtrl: AlertController, private readonly errorService: ErrorService, private readonly loader: LoadingService, private readonly formDialog: FormDialogService, ) {} - async present( - pkgInfo: { - id: string - title: string - mainStatus: T.MainStatus['main'] - }, - actionInfo: { - id: string - metadata: T.ActionMetadata - }, - dependentInfo?: { - title: string - request: T.ActionRequest - }, - ) { + async present(data: PackageActionData) { + const { pkgInfo, actionInfo } = data + if ( allowedStatuses[actionInfo.metadata.allowedStatuses].has( pkgInfo.mainStatus, @@ -61,36 +49,32 @@ export class ActionService { if (actionInfo.metadata.hasInput) { this.formDialog.open(ActionInputModal, { label: actionInfo.metadata.name, - data: { - pkgInfo, - actionInfo: { - id: actionInfo.id, - warning: actionInfo.metadata.warning, - }, - dependentInfo, - }, + data, }) } else { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: `Are you sure you want to execute action "${ - actionInfo.metadata.name - }"? ${actionInfo.metadata.warning || ''}`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Execute', - handler: () => { - this.execute(pkgInfo.id, actionInfo.id) + if (actionInfo.metadata.warning) { + const alert = await this.alertCtrl.create({ + header: 'Warning', + message: actionInfo.metadata.warning, + buttons: [ + { + text: 'Cancel', + role: 'cancel', }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + { + 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]] @@ -123,30 +107,24 @@ export class ActionService { async execute( packageId: string, actionId: string, - inputs?: { - prev: RR.GetActionInputRes - curr: object - }, + input?: object, ): Promise { - const loader = this.loader.open('Executing action...').subscribe() + const loader = this.loader.open('Loading...').subscribe() try { const res = await this.api.runAction({ packageId, actionId, - prev: inputs?.prev || null, - input: inputs?.curr || null, + input: input || null, }) if (res) { - const successModal = await this.modalCtrl.create({ - component: ActionSuccessPage, - componentProps: { - actionRes: res, - }, - }) - - setTimeout(() => successModal.present(), 500) + this.dialogs + .open(new PolymorpheusComponent(ActionSuccessPage), { + label: res.name, + data: res, + }) + .subscribe() } return true // needed to dismiss original modal/alert } catch (e: any) { diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index e3aa63ee3..d6c27e353 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -2,18 +2,13 @@ import { InstalledState, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types' +import { NotificationLevel, 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/util/configBuilderToSpec' 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, @@ -880,25 +875,6 @@ export module Mock { } } - export function getAppMetrics() { - const metr: Metric = { - Metric1: { - value: Math.random(), - unit: 'mi/b', - }, - Metric2: { - value: Math.random(), - unit: '%', - }, - Metric3: { - value: 10.1, - unit: '%', - }, - } - - return metr - } - export const ServerLogs: Log[] = [ { timestamp: '2022-07-28T03:52:54.808769Z', @@ -946,15 +922,6 @@ export module Mock { }, } - export const ActionResponse: T.ActionResult = { - version: '0', - 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(), @@ -1082,11 +1049,26 @@ export module Mock { }, } - export const PackageProperties: RR.GetPackagePropertiesRes<2> = { - version: 2, - data: { - lndconnect: { + export const ActionRes: RR.ActionRes = { + version: '1', + type: 'string', + name: 'New Password', + description: + 'Action was run successfully Action was run successfully Action was run successfully Action was run successfully Action was run successfully', + copyable: true, + qr: true, + masked: true, + value: 'iwejdoiewdhbew', + } + + export const ActionProperties: RR.ActionRes = { + version: '1', + type: 'object', + name: 'Properties', + value: [ + { type: 'string', + name: 'LND Connect', description: 'This is some information about the thing.', copyable: true, qr: true, @@ -1094,45 +1076,50 @@ export module Mock { value: 'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA', }, - Nested: { + { type: 'object', + name: 'Nested Stuff', description: 'This is a nested thing metric', - value: { - 'Last Name': { + value: [ + { type: 'string', + name: 'Last Name', description: 'The last name of the user', copyable: true, qr: true, masked: false, value: 'Hill', }, - Age: { + { type: 'string', + name: 'Age', description: 'The age of the user', copyable: false, qr: false, masked: false, value: '35', }, - Password: { + { type: 'string', + name: 'Password', description: 'A secret password', copyable: true, qr: false, masked: true, value: 'password123', }, - }, + ], }, - 'Another Value': { + { type: 'string', + 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 => @@ -1692,7 +1679,7 @@ export module Mock { }, actions: { config: { - name: 'Bitcoin Config', + name: 'Set Config', description: 'edit bitcoin.conf', warning: null, visibility: 'enabled', @@ -1700,6 +1687,25 @@ export module Mock { 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: 'enabled', + allowedStatuses: 'only-running', + hasInput: false, + group: null, + }, }, serviceInterfaces: { ui: { @@ -1859,7 +1865,27 @@ export module Mock { storeExposedDependents: [], registry: 'https://registry.start9.com/', developerKey: 'developer-key', - requestedActions: {}, + 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 = { @@ -1992,7 +2018,27 @@ export module Mock { storeExposedDependents: [], registry: 'https://registry.start9.com/', developerKey: 'developer-key', - requestedActions: {}, + 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 } = diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 65997c49d..fcf375913 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,5 +1,4 @@ import { Dump } from 'patch-db-client' -import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' import { IST, T } from '@start9labs/start-sdk' @@ -209,19 +208,12 @@ export module RR { // package - export type GetPackagePropertiesReq = { id: string } // package.properties - export type GetPackagePropertiesRes = - PackagePropertiesVersioned - 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 GetPackageMetricsReq = { id: string } // package.metrics - export type GetPackageMetricsRes = Metric - export type InstallPackageReq = T.InstallParams export type InstallPackageRes = null @@ -231,13 +223,12 @@ export module RR { value: object | null } - export type RunActionReq = { + export type ActionReq = { packageId: string actionId: string - prev: GetActionInputRes | null input: object | null } // package.action.run - export type RunActionRes = T.ActionResult | null + export type ActionRes = (T.ActionResult & { version: '1' }) | null export type RestorePackagesReq = { // package.backup.restore @@ -494,7 +485,7 @@ export type DependencyError = | DependencyErrorNotInstalled | DependencyErrorNotRunning | DependencyErrorIncorrectVersion - | DependencyErrorConfigUnsatisfied + | DependencyErrorActionRequired | DependencyErrorHealthChecksFailed | DependencyErrorTransitive @@ -512,8 +503,8 @@ export interface DependencyErrorIncorrectVersion { received: string // version } -export interface DependencyErrorConfigUnsatisfied { - type: 'configUnsatisfied' +export interface DependencyErrorActionRequired { + type: 'actionRequired' } export interface DependencyErrorHealthChecksFailed { diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 49571d97f..280ac79f3 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -113,10 +113,6 @@ export abstract class ApiService { params: RR.GetServerMetricsReq, ): Promise - abstract getPkgMetrics( - params: RR.GetPackageMetricsReq, - ): Promise - abstract updateServer(url?: string): Promise abstract restartServer( @@ -215,10 +211,6 @@ export abstract class ApiService { // package - abstract getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> - abstract getPackageLogs( params: RR.GetPackageLogsReq, ): Promise @@ -235,7 +227,7 @@ export abstract class ApiService { params: RR.GetActionInputReq, ): Promise - abstract runAction(params: RR.RunActionReq): Promise + abstract runAction(params: RR.ActionReq): Promise abstract restorePackages( params: RR.RestorePackagesReq, diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 73f3e67c6..6241eaad7 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -10,7 +10,6 @@ import { import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source' import { ApiService } from './embassy-api.service' import { RR } from './api.types' -import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { ConfigService } from '../config.service' import { webSocket } from 'rxjs/webSocket' import { Observable, filter, firstValueFrom } from 'rxjs' @@ -436,14 +435,6 @@ export class LiveApiService extends ApiService { // package - async getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> { - return this.rpcRequest({ method: 'package.properties', params }).then( - parsePropertiesPermissive, - ) - } - async getPackageLogs( params: RR.GetPackageLogsReq, ): Promise { @@ -456,12 +447,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'package.logs.follow', params }) } - async getPkgMetrics( - params: RR.GetPackageMetricsReq, - ): Promise { - return this.rpcRequest({ method: 'package.metrics', params }) - } - async installPackage( params: RR.InstallPackageReq, ): Promise { @@ -474,7 +459,7 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'package.action.get-input', params }) } - async runAction(params: RR.RunActionReq): Promise { + async runAction(params: RR.ActionReq): Promise { return this.rpcRequest({ method: 'package.action.run', params }) } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index c4834c99e..e62094057 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -17,7 +17,6 @@ import { UpdatingState, } from 'src/app/services/patch-db/data-model' import { CifsBackupTarget, RR } from './api.types' -import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' import { @@ -368,13 +367,6 @@ export class MockApiService extends ApiService { return Mock.getServerMetrics() } - async getPkgMetrics( - params: RR.GetServerMetricsReq, - ): Promise { - await pauseFor(2000) - return Mock.getAppMetrics() - } - async updateServer(url?: string): Promise { await pauseFor(2000) const initialProgress = { @@ -707,13 +699,6 @@ export class MockApiService extends ApiService { // package - async getPackageProperties( - params: RR.GetPackagePropertiesReq, - ): Promise['data']> { - await pauseFor(2000) - return parsePropertiesPermissive(Mock.PackageProperties) - } - async getPackageLogs( params: RR.GetPackageLogsReq, ): Promise { @@ -795,9 +780,23 @@ export class MockApiService extends ApiService { } } - async runAction(params: RR.RunActionReq): Promise { + async runAction(params: RR.ActionReq): Promise { await pauseFor(2000) - return Mock.ActionResponse + + if (params.actionId === 'properties') { + return Mock.ActionProperties + } 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.ActionRes + } } async restorePackages( diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 55ea59a77..3a8560336 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -61,6 +61,7 @@ export const mockPatchData: DataModel = { passwordHash: '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', versionCompat: '>=0.3.0 <=0.3.6', + postInitMigrationTodos: [], statusInfo: { backupProgress: null, updated: false, @@ -82,7 +83,6 @@ export const mockPatchData: DataModel = { selected: null, lastRegion: null, }, - postInitMigrationTodos: [], }, packageData: { bitcoind: { @@ -107,7 +107,7 @@ export const mockPatchData: DataModel = { // }, actions: { config: { - name: 'Bitcoin Config', + name: 'Set Config', description: 'edit bitcoin.conf', warning: null, visibility: 'enabled', @@ -115,6 +115,25 @@ export const mockPatchData: DataModel = { 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: 'enabled', + allowedStatuses: 'only-running', + hasInput: false, + group: null, + }, }, serviceInterfaces: { ui: { @@ -274,7 +293,27 @@ export const mockPatchData: DataModel = { storeExposedDependents: [], registry: 'https://registry.start9.com/', developerKey: 'developer-key', - requestedActions: {}, + 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: { @@ -364,7 +403,27 @@ export const mockPatchData: DataModel = { storeExposedDependents: [], registry: 'https://registry.start9.com/', developerKey: 'developer-key', - requestedActions: {}, + 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, + }, + }, + }, + }, + }, }, }, } diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index c4de94609..cd15ebff6 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -101,17 +101,17 @@ export class DepErrorService { } } - // invalid config + // action required if ( Object.values(pkg.requestedActions).some( a => a.active && a.request.packageId === depId && - a.request.actionId === 'config', + a.request.severity === 'critical', ) ) { return { - type: 'configUnsatisfied', + type: 'actionRequired', } } diff --git a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts index 3f2a7b917..44543b15c 100644 --- a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,7 +1,6 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PkgDependencyErrors } from './dep-error.service' import { T } from '@start9labs/start-sdk' -import { getManifest, needsConfig } from '../util/get-package-data' export interface PackageStatus { primary: PrimaryStatus @@ -29,8 +28,12 @@ export function renderPkgStatus( } function getInstalledPrimaryStatus(pkg: T.PackageDataEntry): PrimaryStatus { - if (needsConfig(getManifest(pkg).id, pkg.requestedActions)) { - return 'needsConfig' + if ( + Object.values(pkg.requestedActions).some( + r => r.active && r.request.severity === 'critical', + ) + ) { + return 'actionRequired' } else { return pkg.status.main } @@ -79,7 +82,7 @@ export type PrimaryStatus = | 'restarting' | 'stopped' | 'backingUp' - | 'needsConfig' + | 'actionRequired' | 'error' export type DependencyStatus = 'warning' | 'satisfied' @@ -135,8 +138,8 @@ export const PrimaryRendering: Record = { color: 'success', showDots: false, }, - needsConfig: { - display: 'Needs Config', + actionRequired: { + display: 'Action Required', color: 'warning', showDots: false, }, diff --git a/web/projects/ui/src/app/util/dep-info.ts b/web/projects/ui/src/app/util/dep-info.ts new file mode 100644 index 000000000..cd98c5d4b --- /dev/null +++ b/web/projects/ui/src/app/util/dep-info.ts @@ -0,0 +1,30 @@ +import { + AllPackageData, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' + +export function getDepDetails( + pkg: PackageDataEntry, + allPkgs: AllPackageData, + depId: string, +) { + const { title, icon, versionRange } = pkg.currentDependencies[depId] + + if ( + allPkgs[depId] && + (allPkgs[depId].stateInfo.state === 'installed' || + allPkgs[depId].stateInfo.state === 'updating') + ) { + return { + title: allPkgs[depId].stateInfo.manifest!.title, + icon: allPkgs[depId].icon, + versionRange, + } + } else { + return { + title: title || depId, + icon: icon || 'assets/img/service-icons/fallback.png', + versionRange, + } + } +} diff --git a/web/projects/ui/src/app/util/get-package-data.ts b/web/projects/ui/src/app/util/get-package-data.ts index 7ba7377fc..c4d0cc046 100644 --- a/web/projects/ui/src/app/util/get-package-data.ts +++ b/web/projects/ui/src/app/util/get-package-data.ts @@ -28,18 +28,6 @@ export function getManifest(pkg: PackageDataEntry): T.Manifest { return (pkg.stateInfo as InstallingState).installingInfo.newManifest } -export function needsConfig( - pkgId: string, - requestedActions: PackageDataEntry['requestedActions'], -): boolean { - return Object.values(requestedActions).some( - r => - r.active && - r.request.packageId === pkgId && - r.request.actionId === 'config', - ) -} - export function isInstalled( pkg: PackageDataEntry, ): pkg is PackageDataEntry { diff --git a/web/projects/ui/src/app/util/get-package-info.ts b/web/projects/ui/src/app/util/get-package-info.ts index 3e506ea61..b21b42a37 100644 --- a/web/projects/ui/src/app/util/get-package-info.ts +++ b/web/projects/ui/src/app/util/get-package-info.ts @@ -20,7 +20,7 @@ export function getPackageInfo( primaryRendering, primaryStatus: statuses.primary, error: statuses.health === 'failure' || statuses.dependency === 'warning', - warning: statuses.primary === 'needsConfig', + warning: statuses.primary === 'actionRequired', transitioning: primaryRendering.showDots || statuses.health === 'loading' || diff --git a/web/projects/ui/src/app/util/properties.util.ts b/web/projects/ui/src/app/util/properties.util.ts deleted file mode 100644 index 5afb4bd8b..000000000 --- a/web/projects/ui/src/app/util/properties.util.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { applyOperation } from 'fast-json-patch' -import matches, { - Parser, - shape, - string, - literal, - boolean, - deferred, - dictionary, - anyOf, - number, - arrayOf, -} from 'ts-matches' - -type PropertiesV1 = typeof matchPropertiesV1._TYPE -type PackagePropertiesV1 = PropertiesV1[] -type PackagePropertiesV2 = { - [name: string]: PackagePropertyString | PackagePropertyObject -} -type PackagePropertiesVersionedData = T extends 1 - ? PackagePropertiesV1 - : T extends 2 - ? PackagePropertiesV2 - : never - -type PackagePropertyString = typeof matchPackagePropertyString._TYPE - -export type PackagePropertiesVersioned = { - version: T - data: PackagePropertiesVersionedData -} -export type PackageProperties = PackagePropertiesV2 - -const matchPropertiesV1 = shape( - { - name: string, - value: string, - description: string, - copyable: boolean, - qr: boolean, - }, - ['description', 'copyable', 'qr'], - { copyable: false, qr: false } as const, -) - -const [matchPackagePropertiesV2, setPPV2] = deferred() -const matchPackagePropertyString = shape( - { - type: literal('string'), - description: string, - value: string, - copyable: boolean, - qr: boolean, - masked: boolean, - }, - ['description', 'copyable', 'qr', 'masked'], - { - copyable: false, - qr: false, - masked: false, - } as const, -) -const matchPackagePropertyObject = shape( - { - type: literal('object'), - value: matchPackagePropertiesV2, - description: string, - }, - ['description'], -) - -const matchPropertyV2 = anyOf( - matchPackagePropertyString, - matchPackagePropertyObject, -) -type PackagePropertyObject = typeof matchPackagePropertyObject._TYPE -setPPV2(dictionary([string, matchPropertyV2])) - -const matchPackagePropertiesVersionedV1 = shape({ - version: number, - data: arrayOf(matchPropertiesV1), -}) -const matchPackagePropertiesVersionedV2 = shape({ - version: number, - data: dictionary([string, matchPropertyV2]), -}) - -export function parsePropertiesPermissive( - properties: unknown, - errorCallback: (err: Error) => any = console.warn, -): PackageProperties { - return matches(properties) - .when(matchPackagePropertiesVersionedV1, prop => - parsePropertiesV1Permissive(prop.data, errorCallback), - ) - .when(matchPackagePropertiesVersionedV2, prop => prop.data) - .when(matches.nill, {}) - .defaultToLazy(() => { - errorCallback(new TypeError(`value is not valid`)) - return {} - }) -} - -function parsePropertiesV1Permissive( - properties: unknown, - errorCallback: (err: Error) => any, -): PackageProperties { - if (!Array.isArray(properties)) { - errorCallback(new TypeError(`${properties} is not an array`)) - return {} - } - return properties.reduce( - (prev: PackagePropertiesV2, cur: unknown, idx: number) => { - const result = matchPropertiesV1.enumParsed(cur) - if ('value' in result) { - const value = result.value - prev[value.name] = { - type: 'string', - value: value.value, - description: value.description, - copyable: value.copyable, - qr: value.qr, - masked: false, - } - } else { - const error = result.error - const message = Parser.validatorErrorAsString(error) - const dataPath = error.keys.map(removeQuotes).join('/') - errorCallback(new Error(`/data/${idx}: ${message}`)) - if (dataPath) { - applyOperation(cur, { - op: 'replace', - path: `/${dataPath}`, - value: undefined, - }) - } - } - return prev - }, - {}, - ) -} - -const removeRegex = /('|")/ -function removeQuotes(x: string) { - while (removeRegex.test(x)) { - x = x.replace(removeRegex, '') - } - return x -}