mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +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:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user