mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Convert properties to an action (#2751)
* update actions response types and partially implement in UI * further remove diagnostic ui * convert action response nested to array * prepare action res modal for Alex * ad dproperties action for Bitcoin * feat: add action success dialog (#2753) * feat: add action success dialog * mocks for string action res and hide properties from actions page --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * return null * remove properties from backend * misc fixes * make severity separate argument * rename ActionRequest to ActionRequestOptions * add clearRequests * fix s9pk build * remove config and properties, introduce action requests * better ux, better moocks, include icons * fix dependency types * add variant for versionCompat * fix dep icon display and patch operation display * misc fixes * misc fixes * alpha 12 * honor provided input to set values in action * fix: show full descriptions of action success items (#2758) * fix type * fix: fix build:deps command on Windows (#2752) * fix: fix build:deps command on Windows * fix: add escaped quotes --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> * misc db compatibility fixes --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
@@ -150,15 +150,15 @@ export function makeEffects(context: EffectContext): Effects {
|
||||
stack: new Error().stack,
|
||||
}) as ReturnType<T.Effects["bind"]>
|
||||
},
|
||||
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return rpcRound("clear-bindings", {}) as ReturnType<
|
||||
clearBindings(...[options]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return rpcRound("clear-bindings", { ...options }) as ReturnType<
|
||||
T.Effects["clearBindings"]
|
||||
>
|
||||
},
|
||||
clearServiceInterfaces(
|
||||
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
...[options]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("clear-service-interfaces", {}) as ReturnType<
|
||||
return rpcRound("clear-service-interfaces", { ...options }) as ReturnType<
|
||||
T.Effects["clearServiceInterfaces"]
|
||||
>
|
||||
},
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -135,6 +135,34 @@ type OldGetConfigRes = {
|
||||
spec: OldConfigSpec
|
||||
}
|
||||
|
||||
export type PropertiesValue =
|
||||
| {
|
||||
/** The type of this value, either "string" or "object" */
|
||||
type: "object"
|
||||
/** A nested mapping of values. The user will experience this as a nested page with back button */
|
||||
value: { [k: string]: PropertiesValue }
|
||||
/** (optional) A human readable description of the new set of values */
|
||||
description: string | null
|
||||
}
|
||||
| {
|
||||
/** The type of this value, either "string" or "object" */
|
||||
type: "string"
|
||||
/** The value to display to the user */
|
||||
value: string
|
||||
/** A human readable description of the value */
|
||||
description: string | null
|
||||
/** Whether or not to mask the value, for example, when displaying a password */
|
||||
masked: boolean | null
|
||||
/** Whether or not to include a button for copying the value to clipboard */
|
||||
copyable: boolean | null
|
||||
/** Whether or not to include a button for displaying the value as a QR code */
|
||||
qr: boolean | null
|
||||
}
|
||||
|
||||
export type PropertiesReturn = {
|
||||
[key: string]: PropertiesValue
|
||||
}
|
||||
|
||||
export type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyObject | PackagePropertyString
|
||||
}
|
||||
@@ -157,7 +185,7 @@ export type PackagePropertyObject = {
|
||||
|
||||
const asProperty_ = (
|
||||
x: PackagePropertyString | PackagePropertyObject,
|
||||
): T.PropertiesValue => {
|
||||
): PropertiesValue => {
|
||||
if (x.type === "object") {
|
||||
return {
|
||||
...x,
|
||||
@@ -177,7 +205,7 @@ const asProperty_ = (
|
||||
...x,
|
||||
}
|
||||
}
|
||||
const asProperty = (x: PackagePropertiesV2): T.PropertiesReturn =>
|
||||
const asProperty = (x: PackagePropertiesV2): PropertiesReturn =>
|
||||
Object.fromEntries(
|
||||
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
|
||||
)
|
||||
@@ -214,6 +242,31 @@ const matchProperties = object({
|
||||
data: matchPackageProperties,
|
||||
})
|
||||
|
||||
function convertProperties(
|
||||
name: string,
|
||||
value: PropertiesValue,
|
||||
): T.ActionResultV1 {
|
||||
if (value.type === "string") {
|
||||
return {
|
||||
type: "string",
|
||||
name,
|
||||
description: value.description,
|
||||
copyable: value.copyable || false,
|
||||
masked: value.masked || false,
|
||||
qr: value.qr || false,
|
||||
value: value.value,
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "object",
|
||||
name,
|
||||
description: value.description || undefined,
|
||||
value: Object.entries(value.value).map(([name, value]) =>
|
||||
convertProperties(name, value),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_REGISTRY = "https://registry.start9.com"
|
||||
export class SystemForEmbassy implements System {
|
||||
currentRunning: MainLoop | undefined
|
||||
@@ -245,6 +298,9 @@ export class SystemForEmbassy implements System {
|
||||
await this.dependenciesAutoconfig(effects, depId, null)
|
||||
}
|
||||
}
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
await this.exportActions(effects)
|
||||
await this.exportNetwork(effects)
|
||||
}
|
||||
|
||||
async exit(): Promise<void> {
|
||||
@@ -281,10 +337,15 @@ export class SystemForEmbassy implements System {
|
||||
await effects.setDataVersion({
|
||||
version: ExtendedVersion.parseEmver(this.manifest.version).toString(),
|
||||
})
|
||||
} else {
|
||||
await effects.action.request({
|
||||
packageId: this.manifest.id,
|
||||
actionId: "config",
|
||||
severity: "critical",
|
||||
replayId: "needs-config",
|
||||
reason: "This service must be configured before it can be run",
|
||||
})
|
||||
}
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
await this.exportActions(effects)
|
||||
await this.exportNetwork(effects)
|
||||
}
|
||||
async exportNetwork(effects: Effects) {
|
||||
for (const [id, interfaceValue] of Object.entries(
|
||||
@@ -375,6 +436,8 @@ export class SystemForEmbassy implements System {
|
||||
if (actionId === "config") {
|
||||
const config = await this.getConfig(effects, timeoutMs)
|
||||
return { spec: config.spec, value: config.config }
|
||||
} else if (actionId === "properties") {
|
||||
return null
|
||||
} else {
|
||||
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
|
||||
if (!oldSpec) return null
|
||||
@@ -393,6 +456,17 @@ export class SystemForEmbassy implements System {
|
||||
if (actionId === "config") {
|
||||
await this.setConfig(effects, input, timeoutMs)
|
||||
return null
|
||||
} else if (actionId === "properties") {
|
||||
return {
|
||||
version: "1",
|
||||
type: "object",
|
||||
name: "Properties",
|
||||
description:
|
||||
"Runtime information, credentials, and other values of interest",
|
||||
value: Object.entries(await this.properties(effects, timeoutMs)).map(
|
||||
([name, value]) => convertProperties(name, value),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
return this.action(effects, actionId, input, timeoutMs)
|
||||
}
|
||||
@@ -405,17 +479,21 @@ export class SystemForEmbassy implements System {
|
||||
if (manifest.config) {
|
||||
actions.config = {
|
||||
name: "Configure",
|
||||
description: "Edit the configuration of this service",
|
||||
description: `Customize ${manifest.title}`,
|
||||
"allowed-statuses": ["running", "stopped"],
|
||||
"input-spec": {},
|
||||
implementation: { type: "script", args: [] },
|
||||
}
|
||||
await effects.action.request({
|
||||
packageId: this.manifest.id,
|
||||
actionId: "config",
|
||||
replayId: "needs-config",
|
||||
description: "This service must be configured before it can be run",
|
||||
})
|
||||
}
|
||||
if (manifest.properties) {
|
||||
actions.properties = {
|
||||
name: "Properties",
|
||||
description:
|
||||
"Runtime information, credentials, and other values of interest",
|
||||
"allowed-statuses": ["running", "stopped"],
|
||||
"input-spec": null,
|
||||
implementation: { type: "script", args: [] },
|
||||
}
|
||||
}
|
||||
for (const [actionId, action] of Object.entries(actions)) {
|
||||
const hasRunning = !!action["allowed-statuses"].find(
|
||||
@@ -694,7 +772,7 @@ export class SystemForEmbassy implements System {
|
||||
async properties(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.PropertiesReturn> {
|
||||
): Promise<PropertiesReturn> {
|
||||
// TODO BLU-J set the properties ever so often
|
||||
const setConfigValue = this.manifest.properties
|
||||
if (!setConfigValue) throw new Error("There is no properties")
|
||||
@@ -867,7 +945,8 @@ export class SystemForEmbassy implements System {
|
||||
actionId: "config",
|
||||
packageId: id,
|
||||
replayId: `${id}/config`,
|
||||
description: `Configure this dependency for the needs of ${this.manifest.title}`,
|
||||
severity: "important",
|
||||
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
|
||||
input: {
|
||||
kind: "partial",
|
||||
value: diff.diff,
|
||||
|
||||
@@ -57,12 +57,6 @@ export class SystemForStartOs implements System {
|
||||
effects,
|
||||
}))
|
||||
}
|
||||
properties(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.PropertiesReturn> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
getActionInput(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
|
||||
@@ -8,7 +8,6 @@ export type Procedure =
|
||||
| "/packageUninit"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/properties"
|
||||
| `/actions/${string}/getInput`
|
||||
| `/actions/${string}/run`
|
||||
|
||||
@@ -30,10 +29,6 @@ export type System = {
|
||||
|
||||
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||
restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||
properties(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.PropertiesReturn>
|
||||
runAction(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { T } from "@start9labs/start-sdk"
|
||||
|
||||
const CallbackIdCell = { inc: 0 }
|
||||
const CallbackIdCell = { inc: 1 }
|
||||
|
||||
const callbackRegistry = new FinalizationRegistry(
|
||||
async (options: { cbs: Map<number, Function>; effects: T.Effects }) => {
|
||||
@@ -23,6 +23,7 @@ export class CallbackHolder {
|
||||
return
|
||||
}
|
||||
const id = this.newId()
|
||||
console.error("adding callback", id)
|
||||
this.callbacks.set(id, callback)
|
||||
if (this.effects)
|
||||
callbackRegistry.register(this, {
|
||||
|
||||
@@ -23,8 +23,6 @@ export const jsonPath = some(
|
||||
"/packageUninit",
|
||||
"/backup/create",
|
||||
"/backup/restore",
|
||||
"/actions/metadata",
|
||||
"/properties",
|
||||
),
|
||||
string.refine(isNestedPath, "isNestedPath"),
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<String>,
|
||||
@@ -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<String>,
|
||||
copyable: bool,
|
||||
qr: bool,
|
||||
masked: bool,
|
||||
},
|
||||
Object {
|
||||
name: String,
|
||||
value: Vec<ActionResultV1>,
|
||||
#[ts(optional)]
|
||||
description: Option<String>,
|
||||
},
|
||||
}
|
||||
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::<unicode::Dense1x2>()
|
||||
.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<T: Serialize>(params: WithIoFormat<T>, result: Option<ActionResult>) {
|
||||
let Some(result) = result else {
|
||||
return;
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
}
|
||||
@@ -238,7 +240,12 @@ async fn cli_apply(
|
||||
context,
|
||||
parent_method,
|
||||
method,
|
||||
params: CliApplyParams { expr, path },
|
||||
params:
|
||||
CliApplyParams {
|
||||
allow_model_mismatch,
|
||||
expr,
|
||||
path,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliApplyParams>,
|
||||
) -> Result<(), RpcError> {
|
||||
@@ -253,7 +260,14 @@ async fn cli_apply(
|
||||
&expr,
|
||||
)?;
|
||||
|
||||
Ok::<_, Error>((
|
||||
let value = if allow_model_mismatch {
|
||||
serde_json::from_value::<Value>(res.clone().into()).with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Deserialization,
|
||||
"result does not match database model",
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
to_value(
|
||||
&serde_json::from_value::<model::Database>(res.clone().into()).with_ctx(
|
||||
|_| {
|
||||
@@ -263,9 +277,9 @@ async fn cli_apply(
|
||||
)
|
||||
},
|
||||
)?,
|
||||
)?,
|
||||
(),
|
||||
))
|
||||
)?
|
||||
};
|
||||
Ok::<_, Error>((value, ()))
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
|
||||
@@ -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<String>,
|
||||
pub reason: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub when: Option<ActionRequestTrigger>,
|
||||
#[ts(optional)]
|
||||
pub input: Option<ActionRequestInput>,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
|
||||
@@ -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<C: Context>() -> ParentHandler<C> {
|
||||
.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::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"backup",
|
||||
backup::package_backup::<C>()
|
||||
|
||||
@@ -113,7 +113,7 @@ async fn ws_handler(
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogResponse {
|
||||
entries: Reversible<LogEntry>,
|
||||
pub entries: Reversible<LogEntry>,
|
||||
start_cursor: Option<String>,
|
||||
end_cursor: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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<Value, Error> {
|
||||
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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ pub async fn bind(
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClearBindingsParams {
|
||||
#[serde(default)]
|
||||
pub except: Vec<BindId>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<impl Future<Output = Result<(), Error>> + 'static> {
|
||||
fn destroy(
|
||||
&mut self,
|
||||
error: bool,
|
||||
) -> Option<impl Future<Output = Result<(), Error>> + '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() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Value, Error> {
|
||||
let container = &self.seed.persistent_container;
|
||||
container
|
||||
.execute::<Value>(
|
||||
Guid::new(),
|
||||
ProcedureName::Properties,
|
||||
Value::Null,
|
||||
Some(Duration::from_secs(30)),
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::Unknown)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,7 +52,7 @@ export type Effects = {
|
||||
options: RequestActionParams,
|
||||
): Promise<null>
|
||||
clearRequests(
|
||||
options: { only: ActionId[] } | { except: ActionId[] },
|
||||
options: { only: string[] } | { except: string[] },
|
||||
): Promise<null>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as T from "../types"
|
||||
import * as IST from "../actions/input/inputSpecTypes"
|
||||
import { Action } from "./setupActions"
|
||||
|
||||
export type RunActionInput<Input> =
|
||||
| Input
|
||||
@@ -43,23 +44,62 @@ export const runAction = async <
|
||||
})
|
||||
}
|
||||
}
|
||||
type GetActionInputType<
|
||||
A extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
> = A extends Action<T.ActionId, any, any, infer I> ? I : never
|
||||
|
||||
// prettier-ignore
|
||||
export type ActionRequest<T extends Omit<T.ActionRequest, "packageId">> =
|
||||
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<T.ActionId, any, any, Record<string, unknown>>,
|
||||
> = {
|
||||
kind: "partial"
|
||||
value: Partial<GetActionInputType<T>>
|
||||
}
|
||||
export type ActionRequestOptions<
|
||||
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
> = ActionRequestBase &
|
||||
(
|
||||
| {
|
||||
when?: Exclude<
|
||||
T.ActionRequestTrigger,
|
||||
{ condition: "input-not-matches" }
|
||||
>
|
||||
input?: ActionRequestInput<T>
|
||||
}
|
||||
| {
|
||||
when: T.ActionRequestTrigger & { condition: "input-not-matches" }
|
||||
input: ActionRequestInput<T>
|
||||
}
|
||||
)
|
||||
|
||||
const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
|
||||
actionId: string
|
||||
packageId: string
|
||||
severity: T.ActionSeverity
|
||||
}
|
||||
|
||||
export const requestAction = <
|
||||
T extends Omit<T.ActionRequest, "packageId">,
|
||||
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
>(options: {
|
||||
effects: T.Effects
|
||||
request: ActionRequest<T> & { replayId?: string; packageId: T.PackageId }
|
||||
packageId: T.PackageId
|
||||
action: T
|
||||
severity: T.ActionSeverity
|
||||
options?: ActionRequestOptions<T>
|
||||
}) => {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export type Run<
|
||||
> = (options: {
|
||||
effects: T.Effects
|
||||
input: ExtractInputSpecType<A> & Record<string, any>
|
||||
}) => Promise<T.ActionResult | null>
|
||||
}) => Promise<T.ActionResult | null | void | undefined>
|
||||
export type GetInput<
|
||||
A extends
|
||||
| Record<string, any>
|
||||
@@ -19,7 +19,9 @@ export type GetInput<
|
||||
| InputSpec<Record<string, any>, never>,
|
||||
> = (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | (ExtractInputSpecType<A> & Record<string, any>)>
|
||||
}) => Promise<
|
||||
null | void | undefined | (ExtractInputSpecType<A> & Record<string, any>)
|
||||
>
|
||||
|
||||
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
|
||||
function callMaybeFn<T>(
|
||||
@@ -91,7 +93,7 @@ export class Action<
|
||||
): Action<Id, Store, {}, {}> {
|
||||
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<T.ActionResult | null> {
|
||||
return this.runFn(options)
|
||||
return (await this.runFn(options)) || 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
|
||||
},
|
||||
) {}
|
||||
}
|
||||
@@ -1,22 +1,11 @@
|
||||
import * as T from "../types"
|
||||
import { once } from "../util"
|
||||
import { Dependency } from "./Dependency"
|
||||
|
||||
type DependencyType<Manifest extends T.Manifest> = {
|
||||
[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<Manifest extends T.SDKManifest> = {
|
||||
[K in keyof Manifest["dependencies"]]: Omit<T.DependencyRequirement, "id">
|
||||
}
|
||||
|
||||
export function setupDependencies<Manifest extends T.Manifest>(
|
||||
export function setupDependencies<Manifest extends T.SDKManifest>(
|
||||
fn: (options: { effects: T.Effects }) => Promise<DependencyType<Manifest>>,
|
||||
): (options: { effects: T.Effects }) => Promise<null> {
|
||||
const cell = { updater: async (_: { effects: T.Effects }) => null }
|
||||
@@ -30,24 +19,12 @@ export function setupDependencies<Manifest extends T.Manifest>(
|
||||
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,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
7
sdk/base/lib/osBindings/ActionResult.ts
Normal file
7
sdk/base/lib/osBindings/ActionResult.ts
Normal file
@@ -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)
|
||||
8
sdk/base/lib/osBindings/ActionResultV0.ts
Normal file
8
sdk/base/lib/osBindings/ActionResultV0.ts
Normal file
@@ -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
|
||||
}
|
||||
18
sdk/base/lib/osBindings/ActionResultV1.ts
Normal file
18
sdk/base/lib/osBindings/ActionResultV1.ts
Normal file
@@ -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<ActionResultV1>
|
||||
description?: string
|
||||
}
|
||||
3
sdk/base/lib/osBindings/ActionSeverity.ts
Normal file
3
sdk/base/lib/osBindings/ActionSeverity.ts
Normal file
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<string, unknown>
|
||||
}) => Promise<ActionResult>
|
||||
export type MaybePromise<A> = Promise<A> | A
|
||||
export namespace ExpectedExports {
|
||||
version: 1
|
||||
@@ -86,10 +82,6 @@ export namespace ExpectedExports {
|
||||
nextVersion: null | string
|
||||
}) => Promise<unknown>
|
||||
|
||||
export type properties = (options: {
|
||||
effects: Effects
|
||||
}) => Promise<PropertiesReturn>
|
||||
|
||||
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<Store = never> = {
|
||||
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<T extends StringObject = Effects> = {
|
||||
[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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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[], Then, Else> =
|
||||
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
|
||||
never
|
||||
|
||||
export class StartSdk<Manifest extends T.Manifest, Store> {
|
||||
export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
private constructor(readonly manifest: Manifest) {}
|
||||
static of() {
|
||||
return new StartSdk<never, never>(null as never)
|
||||
}
|
||||
withManifest<Manifest extends T.Manifest = never>(manifest: Manifest) {
|
||||
withManifest<Manifest extends T.SDKManifest = never>(manifest: Manifest) {
|
||||
return new StartSdk<Manifest, Store>(manifest)
|
||||
}
|
||||
withStore<Store extends Record<string, any>>() {
|
||||
@@ -141,17 +140,39 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
||||
...startSdkEffectWrapper,
|
||||
action: {
|
||||
run: actions.runAction,
|
||||
request: actions.requestAction,
|
||||
requestOwn: <T extends Omit<T.ActionRequest, "packageId">>(
|
||||
request: <
|
||||
T extends Action<T.ActionId, any, any, Record<string, unknown>>,
|
||||
>(
|
||||
effects: T.Effects,
|
||||
request: actions.ActionRequest<T> & {
|
||||
replayId?: string
|
||||
},
|
||||
packageId: T.PackageId,
|
||||
action: T,
|
||||
severity: T.ActionSeverity,
|
||||
options?: actions.ActionRequestOptions<T>,
|
||||
) =>
|
||||
actions.requestAction({
|
||||
effects,
|
||||
request: { ...request, packageId: this.manifest.id },
|
||||
packageId,
|
||||
action,
|
||||
severity,
|
||||
options: options,
|
||||
}),
|
||||
requestOwn: <
|
||||
T extends Action<T.ActionId, Store, any, Record<string, unknown>>,
|
||||
>(
|
||||
effects: T.Effects,
|
||||
action: T,
|
||||
severity: T.ActionSeverity,
|
||||
options?: actions.ActionRequestOptions<T>,
|
||||
) =>
|
||||
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<Manifest extends T.Manifest, Store> {
|
||||
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<Manifest extends T.Manifest, Store> {
|
||||
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||
}) => Promise<Daemons<Manifest, any>>,
|
||||
) => setupMain<Manifest, Store>(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.SdkPropertiesReturn>,
|
||||
): 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<Manifest extends T.Manifest, Store> {
|
||||
* ```
|
||||
*/
|
||||
list: Value.list,
|
||||
hidden: Value.hidden,
|
||||
dynamicToggle: (
|
||||
a: LazyBuild<
|
||||
Store,
|
||||
@@ -1367,7 +1347,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCommand<Manifest extends T.Manifest>(
|
||||
export async function runCommand<Manifest extends T.SDKManifest>(
|
||||
effects: Effects,
|
||||
image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
|
||||
command: string | [string, ...string[]],
|
||||
@@ -1385,26 +1365,3 @@ export async function runCommand<Manifest extends T.Manifest>(
|
||||
(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)]),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export type BackupSync<Volumes extends string> = {
|
||||
* ).build()q
|
||||
* ```
|
||||
*/
|
||||
export class Backups<M extends T.Manifest> {
|
||||
export class Backups<M extends T.SDKManifest> {
|
||||
private constructor(
|
||||
private options = DEFAULT_OPTIONS,
|
||||
private restoreOptions: Partial<T.SyncOptions> = {},
|
||||
@@ -43,7 +43,7 @@ export class Backups<M extends T.Manifest> {
|
||||
private backupSet = [] as BackupSync<M["volumes"][number]>[],
|
||||
) {}
|
||||
|
||||
static withVolumes<M extends T.Manifest = never>(
|
||||
static withVolumes<M extends T.SDKManifest = never>(
|
||||
...volumeNames: Array<M["volumes"][number]>
|
||||
): Backups<M> {
|
||||
return Backups.withSyncs(
|
||||
@@ -54,13 +54,13 @@ export class Backups<M extends T.Manifest> {
|
||||
)
|
||||
}
|
||||
|
||||
static withSyncs<M extends T.Manifest = never>(
|
||||
static withSyncs<M extends T.SDKManifest = never>(
|
||||
...syncs: BackupSync<M["volumes"][number]>[]
|
||||
) {
|
||||
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
|
||||
}
|
||||
|
||||
static withOptions<M extends T.Manifest = never>(
|
||||
static withOptions<M extends T.SDKManifest = never>(
|
||||
options?: Partial<T.SyncOptions>,
|
||||
) {
|
||||
return new Backups<M>({ ...DEFAULT_OPTIONS, ...options })
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Backups } from "./Backups"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { _ } from "../util"
|
||||
|
||||
export type SetupBackupsParams<M extends T.Manifest> =
|
||||
export type SetupBackupsParams<M extends T.SDKManifest> =
|
||||
| M["volumes"][number][]
|
||||
| ((_: { effects: T.Effects }) => Promise<Backups<M>>)
|
||||
|
||||
@@ -11,7 +11,7 @@ type SetupBackupsRes = {
|
||||
restoreBackup: T.ExpectedExports.restoreBackup
|
||||
}
|
||||
|
||||
export function setupBackups<M extends T.Manifest>(
|
||||
export function setupBackups<M extends T.SDKManifest>(
|
||||
options: SetupBackupsParams<M>,
|
||||
) {
|
||||
let backupsFactory: (_: { effects: T.Effects }) => Promise<Backups<M>>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,12 +7,14 @@ import { VersionGraph } from "../version/VersionGraph"
|
||||
import { Install } from "./setupInstall"
|
||||
import { Uninstall } from "./setupUninstall"
|
||||
|
||||
export function setupInit<Manifest extends T.Manifest, Store>(
|
||||
versions: VersionGraph<Manifest["version"]>,
|
||||
export function setupInit<Manifest extends T.SDKManifest, Store>(
|
||||
versions: VersionGraph<string>,
|
||||
install: Install<Manifest, Store>,
|
||||
uninstall: Uninstall<Manifest, Store>,
|
||||
setServiceInterfaces: UpdateServiceInterfaces<any>,
|
||||
setDependencies: (options: { effects: T.Effects }) => Promise<null>,
|
||||
setDependencies: (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined>,
|
||||
actions: Actions<Store, any>,
|
||||
exposedStore: ExposedStorePaths,
|
||||
): {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
export type InstallFn<Manifest extends T.Manifest, Store> = (opts: {
|
||||
export type InstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null>
|
||||
export class Install<Manifest extends T.Manifest, Store> {
|
||||
}) => Promise<null | void | undefined>
|
||||
export class Install<Manifest extends T.SDKManifest, Store> {
|
||||
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
|
||||
static of<Manifest extends T.Manifest, Store>(
|
||||
static of<Manifest extends T.SDKManifest, Store>(
|
||||
fn: InstallFn<Manifest, Store>,
|
||||
) {
|
||||
return new Install(fn)
|
||||
@@ -18,7 +18,7 @@ export class Install<Manifest extends T.Manifest, Store> {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupInstall<Manifest extends T.Manifest, Store>(
|
||||
export function setupInstall<Manifest extends T.SDKManifest, Store>(
|
||||
fn: InstallFn<Manifest, Store>,
|
||||
) {
|
||||
return Install.of(fn)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
export type UninstallFn<Manifest extends T.Manifest, Store> = (opts: {
|
||||
export type UninstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null>
|
||||
export class Uninstall<Manifest extends T.Manifest, Store> {
|
||||
}) => Promise<null | void | undefined>
|
||||
export class Uninstall<Manifest extends T.SDKManifest, Store> {
|
||||
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
|
||||
static of<Manifest extends T.Manifest, Store>(
|
||||
static of<Manifest extends T.SDKManifest, Store>(
|
||||
fn: UninstallFn<Manifest, Store>,
|
||||
) {
|
||||
return new Uninstall(fn)
|
||||
@@ -22,7 +22,7 @@ export class Uninstall<Manifest extends T.Manifest, Store> {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupUninstall<Manifest extends T.Manifest, Store>(
|
||||
export function setupUninstall<Manifest extends T.SDKManifest, Store>(
|
||||
fn: UninstallFn<Manifest, Store>,
|
||||
) {
|
||||
return Uninstall.of(fn)
|
||||
|
||||
@@ -20,7 +20,7 @@ export class CommandController {
|
||||
private process: cp.ChildProcessWithoutNullStreams,
|
||||
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
||||
) {}
|
||||
static of<Manifest extends T.Manifest>() {
|
||||
static of<Manifest extends T.SDKManifest>() {
|
||||
return async <A extends string>(
|
||||
effects: T.Effects,
|
||||
subcontainer:
|
||||
|
||||
@@ -17,7 +17,7 @@ export class Daemon {
|
||||
get subContainerHandle(): undefined | ExecSpawnable {
|
||||
return this.commandController?.subContainerHandle
|
||||
}
|
||||
static of<Manifest extends T.Manifest>() {
|
||||
static of<Manifest extends T.SDKManifest>() {
|
||||
return async <A extends string>(
|
||||
effects: T.Effects,
|
||||
subcontainer:
|
||||
|
||||
@@ -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<Id extends string> = `The id '${Id}' is already used`
|
||||
|
||||
export const runCommand = <Manifest extends T.Manifest>() =>
|
||||
export const runCommand = <Manifest extends T.SDKManifest>() =>
|
||||
CommandController.of<Manifest>()
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ Daemons.of({
|
||||
})
|
||||
```
|
||||
*/
|
||||
export class Daemons<Manifest extends T.Manifest, Ids extends string>
|
||||
export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
implements T.DaemonBuildable
|
||||
{
|
||||
private constructor(
|
||||
@@ -89,7 +89,7 @@ export class Daemons<Manifest extends T.Manifest, Ids extends string>
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
static of<Manifest extends T.Manifest>(options: {
|
||||
static of<Manifest extends T.SDKManifest>(options: {
|
||||
effects: T.Effects
|
||||
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>
|
||||
healthReceipts: HealthReceipt[]
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MountOptions } from "../util/SubContainer"
|
||||
|
||||
type MountArray = { path: string; options: MountOptions }[]
|
||||
|
||||
export class Mounts<Manifest extends T.Manifest> {
|
||||
export class Mounts<Manifest extends T.SDKManifest> {
|
||||
private constructor(
|
||||
readonly volumes: {
|
||||
id: Manifest["volumes"][number]
|
||||
@@ -25,7 +25,7 @@ export class Mounts<Manifest extends T.Manifest> {
|
||||
}[],
|
||||
) {}
|
||||
|
||||
static of<Manifest extends T.Manifest>() {
|
||||
static of<Manifest extends T.SDKManifest>() {
|
||||
return new Mounts<Manifest>([], [], [])
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class Mounts<Manifest extends T.Manifest> {
|
||||
return this
|
||||
}
|
||||
|
||||
addDependency<DependencyManifest extends T.Manifest>(
|
||||
addDependency<DependencyManifest extends T.SDKManifest>(
|
||||
dependencyId: keyof Manifest["dependencies"] & string,
|
||||
volumeId: DependencyManifest["volumes"][number],
|
||||
subpath: string | null,
|
||||
|
||||
@@ -14,7 +14,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000
|
||||
* @param fn
|
||||
* @returns
|
||||
*/
|
||||
export const setupMain = <Manifest extends T.Manifest, Store>(
|
||||
export const setupMain = <Manifest extends T.SDKManifest, Store>(
|
||||
fn: (o: {
|
||||
effects: T.Effects
|
||||
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
VolumesTypes extends VolumeId,
|
||||
AssetTypes extends VolumeId,
|
||||
ImagesTypes extends ImageId,
|
||||
Manifest extends {
|
||||
dependencies: Dependencies
|
||||
id: Id
|
||||
assets: AssetTypes[]
|
||||
images: Record<ImagesTypes, SDKImageInputSpec>
|
||||
volumes: VolumesTypes[]
|
||||
},
|
||||
>(manifest: SDKManifest & Manifest): SDKManifest & Manifest {
|
||||
return manifest
|
||||
}
|
||||
|
||||
export function buildManifest<
|
||||
Id extends string,
|
||||
Version extends string,
|
||||
Dependencies extends Record<string, unknown>,
|
||||
@@ -27,7 +44,6 @@ export function setupManifest<
|
||||
images: Record<ImagesTypes, SDKImageInputSpec>
|
||||
volumes: VolumesTypes[]
|
||||
},
|
||||
Satisfies extends string[] = [],
|
||||
>(
|
||||
versions: VersionGraph<Version>,
|
||||
manifest: SDKManifest & Manifest,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -86,19 +86,21 @@ export class FileHelper<A> {
|
||||
/**
|
||||
* Accepts structured data and overwrites the existing file on disk.
|
||||
*/
|
||||
async write(data: A) {
|
||||
async write(data: A): Promise<null> {
|
||||
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<A | null> {
|
||||
if (!(await exists(this.path))) {
|
||||
return null
|
||||
}
|
||||
@@ -107,14 +109,14 @@ export class FileHelper<A> {
|
||||
)
|
||||
}
|
||||
|
||||
async const(effects: T.Effects) {
|
||||
const watch = this.watch()
|
||||
private async readConst(effects: T.Effects): Promise<A | null> {
|
||||
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<A> {
|
||||
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<A> {
|
||||
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)
|
||||
}
|
||||
|
||||
33
sdk/package/package-lock.json
generated
33
sdk/package/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -83,7 +83,7 @@ export class SuccessPage {
|
||||
await this.api.exit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
await this.errorService.handleError(e)
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,7 +65,6 @@ const ICONS = [
|
||||
'options-outline',
|
||||
'pencil',
|
||||
'phone-portrait-outline',
|
||||
'play-circle-outline',
|
||||
'play-outline',
|
||||
'power',
|
||||
'pricetag-outline',
|
||||
|
||||
@@ -149,14 +149,15 @@ export class FormComponent<T extends Record<string, any>> 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()
|
||||
|
||||
@@ -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: `
|
||||
<div class="service-title">
|
||||
<img [src]="pkgInfo.icon" alt="" />
|
||||
<h4>{{ pkgInfo.title }}</h4>
|
||||
</div>
|
||||
<ng-container *ngIf="res$ | async as res; else loading">
|
||||
<tui-notification *ngIf="error" status="error">
|
||||
<div [innerHTML]="error"></div>
|
||||
@@ -52,13 +58,11 @@ export interface PackageActionData {
|
||||
<div [innerHTML]="warning"></div>
|
||||
</tui-notification>
|
||||
|
||||
<action-dep
|
||||
*ngIf="dependentInfo"
|
||||
[pkgTitle]="pkgInfo.title"
|
||||
[depTitle]="dependentInfo.title"
|
||||
<action-request-info
|
||||
*ngIf="requestInfo"
|
||||
[originalValue]="res.originalValue || {}"
|
||||
[operations]="res.operations || []"
|
||||
></action-dep>
|
||||
></action-request-info>
|
||||
|
||||
<app-form
|
||||
tuiMode="onDark"
|
||||
@@ -87,7 +91,19 @@ export interface PackageActionData {
|
||||
`
|
||||
tui-notification {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
.service-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.4rem;
|
||||
img {
|
||||
height: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
@@ -98,7 +114,7 @@ export interface PackageActionData {
|
||||
TuiNotificationModule,
|
||||
TuiButtonModule,
|
||||
TuiModeModule,
|
||||
ActionDepComponent,
|
||||
ActionRequestInfoComponent,
|
||||
UiPipeModule,
|
||||
FormComponent,
|
||||
],
|
||||
@@ -106,9 +122,9 @@ export interface PackageActionData {
|
||||
})
|
||||
export class ActionInputModal {
|
||||
readonly actionId = this.context.data.actionInfo.id
|
||||
readonly warning = this.context.data.actionInfo.warning
|
||||
readonly warning = this.context.data.actionInfo.metadata.warning
|
||||
readonly pkgInfo = this.context.data.pkgInfo
|
||||
readonly dependentInfo = this.context.data.dependentInfo
|
||||
readonly requestInfo = this.context.data.requestInfo
|
||||
|
||||
buttons: ActionButton<any>[] = [
|
||||
{
|
||||
@@ -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' &&
|
||||
|
||||
@@ -11,14 +11,10 @@ import { CommonModule } from '@angular/common'
|
||||
import { TuiNotificationModule } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'action-dep',
|
||||
selector: 'action-request-info',
|
||||
template: `
|
||||
<tui-notification>
|
||||
<h3 style="margin: 0 0 0.5rem; font-size: 1.25rem;">
|
||||
{{ pkgTitle }}
|
||||
</h3>
|
||||
The following modifications have been made to {{ pkgTitle }} to satisfy
|
||||
{{ depTitle }}:
|
||||
<tui-notification *ngIf="diff.length">
|
||||
The following modifications were made:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHTML]="d"></li>
|
||||
</ul>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<p *ngFor="let item of value?.value">
|
||||
<app-action-success-item
|
||||
*ngIf="isSingle(item)"
|
||||
[value]="item"
|
||||
></app-action-success-item>
|
||||
<tui-accordion-item *ngIf="!isSingle(item)" size="s">
|
||||
<div tuiFade>{{ item.name }}</div>
|
||||
<ng-template tuiAccordionItemContent>
|
||||
<app-action-success-group [value]="item"></app-action-success-group>
|
||||
</ng-template>
|
||||
</tui-accordion-item>
|
||||
</p>
|
||||
`,
|
||||
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'
|
||||
}
|
||||
}
|
||||
@@ -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: `
|
||||
<p *ngIf="!parent" class="qr">
|
||||
<ng-container *ngTemplateOutlet="qr"></ng-container>
|
||||
</p>
|
||||
<label [tuiLabel]="value.description">
|
||||
<tui-input
|
||||
[readOnly]="true"
|
||||
[ngModel]="value.value"
|
||||
[tuiTextfieldCustomContent]="actions"
|
||||
>
|
||||
<input
|
||||
tuiTextfield
|
||||
[style.border-inline-end-width.rem]="border"
|
||||
[type]="value.masked && masked ? 'password' : 'text'"
|
||||
/>
|
||||
</tui-input>
|
||||
</label>
|
||||
<ng-template #actions>
|
||||
<button
|
||||
*ngIf="value.masked"
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
size="s"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="masked = !masked"
|
||||
>
|
||||
Reveal/Hide
|
||||
</button>
|
||||
<button
|
||||
*ngIf="value.copyable"
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
size="s"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
iconLeft="tuiIconCopy"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="copy()"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
*ngIf="value.qr && parent"
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
size="s"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
iconLeft="tuiIconGrid"
|
||||
[style.pointer-events]="'auto'"
|
||||
(click)="show(qr)"
|
||||
>
|
||||
Show QR
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #qr>
|
||||
<qr-code
|
||||
[value]="value.value"
|
||||
[style.filter]="value.masked && masked ? 'blur(0.5rem)' : null"
|
||||
size="350"
|
||||
></qr-code>
|
||||
<button
|
||||
*ngIf="value.masked && masked"
|
||||
tuiIconButton
|
||||
class="reveal"
|
||||
iconLeft="tuiIconEye"
|
||||
[style.border-radius.%]="100"
|
||||
(click)="masked = false"
|
||||
>
|
||||
Reveal
|
||||
</button>
|
||||
</ng-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<HTMLInputElement>
|
||||
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<any>) {
|
||||
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'
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -1,35 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Execution Complete</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()" class="enter-click">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<h2 class="ion-padding">{{ actionRes.message }}</h2>
|
||||
|
||||
<div *ngIf="actionRes.value" class="ion-text-center" style="padding: 48px 0">
|
||||
<div *ngIf="actionRes.qr" class="ion-padding-bottom">
|
||||
<qr-code [value]="actionRes.value" size="240"></qr-code>
|
||||
</div>
|
||||
|
||||
<p *ngIf="!actionRes.copyable">{{ actionRes.value }}</p>
|
||||
<a
|
||||
*ngIf="actionRes.copyable"
|
||||
style="cursor: copy"
|
||||
(click)="copy(actionRes.value)"
|
||||
>
|
||||
<b>{{ actionRes.value }}</b>
|
||||
<sup>
|
||||
<ion-icon
|
||||
name="copy-outline"
|
||||
style="padding-left: 6px; font-size: small"
|
||||
></ion-icon>
|
||||
</sup>
|
||||
</a>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -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: `
|
||||
<ng-container tuiTextfieldSize="m" [tuiTextfieldLabelOutside]="true">
|
||||
<app-action-success-item
|
||||
*ngIf="item"
|
||||
[value]="item"
|
||||
></app-action-success-item>
|
||||
<app-action-success-group
|
||||
*ngIf="group"
|
||||
[value]="group"
|
||||
></app-action-success-group>
|
||||
</ng-container>
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ActionSuccessGroupComponent,
|
||||
ActionSuccessItemComponent,
|
||||
TuiTextfieldControllerModule,
|
||||
],
|
||||
})
|
||||
export class ActionSuccessPage {
|
||||
@Input()
|
||||
actionRes!: T.ActionResult
|
||||
readonly data =
|
||||
inject<TuiDialogContext<void, RR.ActionRes>>(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
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
<app-actions-item
|
||||
*ngFor="let action of pkg.actions"
|
||||
[action]="action"
|
||||
icon="play-circle-outline"
|
||||
(click)="handleAction(pkg.mainStatus, pkg.manifest, action)"
|
||||
icon="play-outline"
|
||||
(click)="handleAction(pkg.mainStatus, pkg.icon, pkg.manifest, action)"
|
||||
></app-actions-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
@@ -1,25 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Monitor</ion-title>
|
||||
<ion-title slot="end">
|
||||
<ion-spinner name="dots" class="fader"></ion-spinner>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<skeleton-list *ngIf="loading"></skeleton-list>
|
||||
<ion-item-group *ngIf="!loading">
|
||||
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
|
||||
<ion-label>{{ metric.key }}</ion-label>
|
||||
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
|
||||
<ion-text style="color: white">
|
||||
{{ metric.value.value }} {{ metric.value.unit }}
|
||||
</ion-text>
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -1,119 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Properties</ion-title>
|
||||
<ion-buttons *ngIf="!loading" slot="end">
|
||||
<ion-button (click)="refresh()">
|
||||
<ion-icon slot="start" name="refresh"></ion-icon>
|
||||
Refresh
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<text-spinner
|
||||
*ngIf="loading; else loaded"
|
||||
text="Loading Properties"
|
||||
></text-spinner>
|
||||
|
||||
<ng-template #loaded>
|
||||
<!-- not running -->
|
||||
<ion-item *ngIf="stopped$ | async" class="ion-margin-bottom">
|
||||
<ion-label>
|
||||
<p>
|
||||
<ion-text color="warning">
|
||||
Service is stopped. Information on this page could be inaccurate.
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- no properties -->
|
||||
<ion-item *ngIf="properties | empty">
|
||||
<ion-label>
|
||||
<p>No properties.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- properties -->
|
||||
<ion-item-group *ngIf="!(properties | empty)">
|
||||
<div *ngFor="let prop of node | keyvalue: asIsOrder">
|
||||
<!-- object -->
|
||||
<ion-item
|
||||
button
|
||||
detail="true"
|
||||
*ngIf="prop.value.type === 'object'"
|
||||
(click)="goToNested(prop.key)"
|
||||
>
|
||||
<ion-button
|
||||
*ngIf="prop.value.description"
|
||||
fill="clear"
|
||||
slot="start"
|
||||
(click)="presentDescription(prop, $event)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<h2>{{ prop.key }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- not object -->
|
||||
<ion-item *ngIf="prop.value.type === 'string'">
|
||||
<ion-button
|
||||
*ngIf="prop.value.description"
|
||||
fill="clear"
|
||||
slot="start"
|
||||
(click)="presentDescription(prop, $event)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="help-circle-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-label>
|
||||
<h2>{{ prop.key }}</h2>
|
||||
<p class="courier-new">
|
||||
{{ prop.value.masked && !unmasked[prop.key] ? (prop.value.value |
|
||||
mask : 64) : prop.value.value }}
|
||||
</p>
|
||||
</ion-label>
|
||||
<div slot="end" *ngIf="prop.value.copyable || prop.value.qr">
|
||||
<ion-button
|
||||
*ngIf="prop.value.masked"
|
||||
fill="clear"
|
||||
(click)="toggleMask(prop.key)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked[prop.key] ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="prop.value.qr"
|
||||
fill="clear"
|
||||
(click)="showQR(prop.value.value)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="qr-code-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
*ngIf="prop.value.copyable"
|
||||
fill="clear"
|
||||
(click)="copy(prop.value.value)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
name="copy-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
.metric-note {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -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<DataModel>,
|
||||
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<any> {
|
||||
this.navCtrl.navigateForward(`/services/${this.pkgId}/properties`, {
|
||||
queryParams: {
|
||||
pointer: `${this.pointer}/${key}/value`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async copy(text: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<ng-container
|
||||
*ngIf="isInstalled(pkg) && status.primary !== 'backingUp' && status.primary !== 'error'"
|
||||
>
|
||||
<!-- ** action requests ** -->
|
||||
<app-show-action-requests
|
||||
[allPkgs]="pkgPlus.allPkgs"
|
||||
[pkg]="pkg"
|
||||
[manifest]="pkgPlus.manifest"
|
||||
></app-show-action-requests>
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="pkg.status.main === 'running'"
|
||||
|
||||
@@ -20,14 +20,13 @@ import {
|
||||
import { combineLatest } from 'rxjs'
|
||||
import {
|
||||
getManifest,
|
||||
getPackage,
|
||||
isInstalled,
|
||||
isInstalling,
|
||||
isRestoring,
|
||||
isUpdating,
|
||||
} from 'src/app/util/get-package-data'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { getDepDetails } from 'src/app/util/dep-info'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
@@ -35,7 +34,7 @@ export interface DependencyInfo {
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
actionText: string | null
|
||||
action: () => 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<DataModel>,
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<ng-container *ngIf="actionRequests.critical.length">
|
||||
<ion-item-divider>Required Actions</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let request of actionRequests.critical"
|
||||
button
|
||||
(click)="handleAction(request)"
|
||||
>
|
||||
<ion-icon slot="start" name="warning-outline" color="warning"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="highlighted">{{ request.actionName }}</h2>
|
||||
<p *ngIf="request.dependency" class="dependency">
|
||||
<span class="light">Service:</span>
|
||||
<img [src]="request.dependency.icon" alt="" />
|
||||
{{ request.dependency.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="light">Reason:</span>
|
||||
{{ request.reason || 'no reason provided' }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="actionRequests.important.length">
|
||||
<ion-item-divider>Requested Actions</ion-item-divider>
|
||||
<ion-item
|
||||
*ngFor="let request of actionRequests.important"
|
||||
button
|
||||
(click)="handleAction(request)"
|
||||
>
|
||||
<ion-icon slot="start" name="play-outline" color="warning"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 class="highlighted">{{ request.actionName }}</h2>
|
||||
<p *ngIf="request.dependency" class="dependency">
|
||||
<span class="light">Service:</span>
|
||||
<img [src]="request.dependency.icon" alt="" />
|
||||
{{ request.dependency.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="light">Reason:</span>
|
||||
{{ request.reason || 'no reason provided' }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, T.PackageDataEntry>
|
||||
|
||||
@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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<ion-item-divider>Dependencies</ion-item-divider>
|
||||
<!-- dependencies are a subset of the pkg.manifest.dependencies that are currently required as determined by the service config -->
|
||||
<ion-item button *ngFor="let dep of dependencies" (click)="dep.action()">
|
||||
<ion-item
|
||||
[button]="!!dep.actionText"
|
||||
*ngFor="let dep of dependencies"
|
||||
(click)="dep.action()"
|
||||
>
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="dep.icon" alt="" />
|
||||
</ion-thumbnail>
|
||||
|
||||
@@ -52,16 +52,6 @@
|
||||
Start
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="needsConfig(manifest.id, pkg.requestedActions)"
|
||||
class="action-button"
|
||||
color="warning"
|
||||
(click)="presentModalConfig()"
|
||||
>
|
||||
<ion-icon slot="start" name="construct-outline"></ion-icon>
|
||||
Configure
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="pkgStatus && interfaces && (interfaces | hasUi) && hosts"
|
||||
class="action-button"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AlertController } from '@ionic/angular'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import {
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
getAllPackages,
|
||||
getManifest,
|
||||
isInstalled,
|
||||
needsConfig,
|
||||
} from 'src/app/util/get-package-data'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
|
||||
@@ -39,7 +37,6 @@ export class AppShowStatusComponent {
|
||||
PR = PrimaryRendering
|
||||
|
||||
isInstalled = isInstalled
|
||||
needsConfig = needsConfig
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
@@ -49,7 +46,6 @@ export class AppShowStatusComponent {
|
||||
private readonly launcherService: UiLauncherService,
|
||||
readonly connection$: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
const alert = await this.alertCtrl.create({
|
||||
|
||||
@@ -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<DataModel>,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry<InstalledState>): 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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<PackageActionData>(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<boolean> {
|
||||
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) {
|
||||
|
||||
@@ -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<IST.InputSpec> =>
|
||||
@@ -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<InstalledState> = {
|
||||
@@ -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<InstalledState> } =
|
||||
|
||||
@@ -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<T extends number> =
|
||||
PackagePropertiesVersioned<T>
|
||||
|
||||
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 {
|
||||
|
||||
@@ -113,10 +113,6 @@ export abstract class ApiService {
|
||||
params: RR.GetServerMetricsReq,
|
||||
): Promise<RR.GetServerMetricsRes>
|
||||
|
||||
abstract getPkgMetrics(
|
||||
params: RR.GetPackageMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes>
|
||||
|
||||
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
||||
|
||||
abstract restartServer(
|
||||
@@ -215,10 +211,6 @@ export abstract class ApiService {
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']>
|
||||
|
||||
abstract getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes>
|
||||
@@ -235,7 +227,7 @@ export abstract class ApiService {
|
||||
params: RR.GetActionInputReq,
|
||||
): Promise<RR.GetActionInputRes>
|
||||
|
||||
abstract runAction(params: RR.RunActionReq): Promise<RR.RunActionRes>
|
||||
abstract runAction(params: RR.ActionReq): Promise<RR.ActionRes>
|
||||
|
||||
abstract restorePackages(
|
||||
params: RR.RestorePackagesReq,
|
||||
|
||||
@@ -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<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
return this.rpcRequest({ method: 'package.properties', params }).then(
|
||||
parsePropertiesPermissive,
|
||||
)
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
@@ -456,12 +447,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.logs.follow', params })
|
||||
}
|
||||
|
||||
async getPkgMetrics(
|
||||
params: RR.GetPackageMetricsReq,
|
||||
): Promise<RR.GetPackageMetricsRes> {
|
||||
return this.rpcRequest({ method: 'package.metrics', params })
|
||||
}
|
||||
|
||||
async installPackage(
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes> {
|
||||
@@ -474,7 +459,7 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.action.get-input', params })
|
||||
}
|
||||
|
||||
async runAction(params: RR.RunActionReq): Promise<RR.RunActionRes> {
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
return this.rpcRequest({ method: 'package.action.run', params })
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RR.GetPackageMetricsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.getAppMetrics()
|
||||
}
|
||||
|
||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||
await pauseFor(2000)
|
||||
const initialProgress = {
|
||||
@@ -707,13 +699,6 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties(
|
||||
params: RR.GetPackagePropertiesReq,
|
||||
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
|
||||
await pauseFor(2000)
|
||||
return parsePropertiesPermissive(Mock.PackageProperties)
|
||||
}
|
||||
|
||||
async getPackageLogs(
|
||||
params: RR.GetPackageLogsReq,
|
||||
): Promise<RR.GetPackageLogsRes> {
|
||||
@@ -795,9 +780,23 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async runAction(params: RR.RunActionReq): Promise<RR.RunActionRes> {
|
||||
async runAction(params: RR.ActionReq): Promise<RR.ActionRes> {
|
||||
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(
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user