Skip to content
Open

Gui #224

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/cli/src/commands/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use commonwl::execution::{ContainerEngine, execute_cwlfile, set_container_engine
use commonwl::prelude::*;
use remote_execution::{check_status, download_results, export_rocrate, logout};
use serde_yaml::{Number, Value};
use std::{collections::HashMap, error::Error, fs, path::PathBuf};
use std::{collections::HashMap, error::Error, fs, path::{Path, PathBuf}};

pub fn handle_execute_commands(subcommand: &ExecuteCommands) -> Result<(), Box<dyn Error>> {
match subcommand {
Expand All @@ -18,8 +18,8 @@ pub fn handle_execute_commands(subcommand: &ExecuteCommands) -> Result<(), Box<d
logout,
} => schedule_run(file, input_file, *rocrate, *watch, *logout),
RemoteSubcommands::Status { workflow_name } => check_status(workflow_name),
RemoteSubcommands::Download { workflow_name, output_dir } => download_results(workflow_name, output_dir.as_ref()),
RemoteSubcommands::Rocrate { workflow_name, output_dir } => export_rocrate(workflow_name, output_dir.as_ref()),
RemoteSubcommands::Download { workflow_name, all, output_dir } => download_results(workflow_name, *all, output_dir.as_ref()),
RemoteSubcommands::Rocrate { workflow_name, output_dir } => export_rocrate(workflow_name, output_dir.as_ref(), None),
RemoteSubcommands::Logout => logout(),
},
ExecuteCommands::MakeTemplate(args) => make_template(&args.cwl),
Expand Down Expand Up @@ -86,10 +86,12 @@ pub enum RemoteSubcommands {
#[arg(help = "Workflow name to check (if omitted, checks all)")]
workflow_name: Option<String>,
},
#[command(about = "Downloads finished Workflow from REANA")]
#[command(about = "Downloads workflow outputs from REANA")]
Download {
#[arg(help = "Workflow name to download results for")]
workflow_name: String,
#[arg(short = 'a', long = "all", help = "Download all files of the workflow")]
all: bool,
#[arg(short = 'd', long = "output_dir", help = "Optional output directory to save downloaded files")]
output_dir: Option<String>,
},
Expand Down Expand Up @@ -128,7 +130,7 @@ pub fn execute_local(args: &LocalExecuteArgs) -> Result<(), ExecutionError> {
execute_cwlfile(&args.file, &args.args, args.out_dir.clone())
}

pub fn schedule_run(file: &PathBuf, input_file: &Option<PathBuf>, rocrate: bool, watch: bool, logout: bool) -> Result<(), Box<dyn Error>> {
pub fn schedule_run(file: &Path, input_file: &Option<PathBuf>, rocrate: bool, watch: bool, logout: bool) -> Result<(), Box<dyn Error>> {
let workflow_name = remote_execution::schedule_run(file, input_file)?;

if watch {
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/visualize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ pub fn render<R: FlowchartRenderer>(r: &mut R, cwl: &Workflow, filename: &Path,

if !no_defaults && let Some(doc) = load_step(step, filename) {
for input in &doc.inputs {
if !step.in_.iter().any(|i| i.id == input.id) && input.default.is_some() {
if let Some(default) = input.default.as_ref()
&& !step.in_.iter().any(|i| i.id == input.id)
{
let node_id = format!("{}_{}", step.id, input.id);
r.node(&node_id, Some(&input.default.as_ref().unwrap().as_value_string()), RenderStyle::Small);
r.edge(&node_id, &step.id, Some(&input.id), RenderStyle::Small);
r.node(&node_id, Some(&default.as_value_string()), RenderStyle::Small);
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ toml.workspace = true
commonwl = { path = "../cwl" }
repository = { path = "../repository" }
s4n_core = { path = "../core" }
remote_execution = { path = "../remote_execution" }
reana = { path = "../reana" }
keyring = { version = "3.6.3", features = [
"apple-native",
"linux-native",
"windows-native",
] }

dioxus = { version = "0.7.2", features = ["router"] }
dioxus-free-icons = { version = "0.10.0", features = [
Expand Down
2 changes: 1 addition & 1 deletion packages/gui/Dioxus.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ icon = ["assets/icon.ico", "assets/icon.png"]
category = "Productivity"

[bundle.windows]
icon_path = "packages/gui/assets/icon.ico"
icon_path = "assets/icon.ico"

[web.app]
title = "SciWIn Studio"
Expand Down
27 changes: 21 additions & 6 deletions packages/gui/src/components/files/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@ pub fn get_route(node: &Node) -> Route {
}

pub fn read_node_type(path: impl AsRef<Path>) -> FileType {
if path.as_ref().is_dir() || path.as_ref().extension() != Some(OsStr::new("cwl")) {
let path = path.as_ref();
if path.is_dir() || path.extension() != Some(OsStr::new("cwl")) {
return FileType::Other;
}
let content = std::fs::read_to_string(path).expect("Can not read file!");
let safe_path = match path.canonicalize() {
Ok(p) => p,
Err(_) => return FileType::Other,
};
let content = match std::fs::read_to_string(&safe_path) {
Ok(c) => c,
Err(_) => return FileType::Other,
};
let yaml: Value = serde_yaml::from_str(&content).unwrap_or(Value::Null);

match yaml.get("class").and_then(|v| v.as_str()) {
Expand All @@ -99,10 +107,17 @@ mod tests {

#[test]
fn test_read_node_type() {
let path = "../../testdata/hello_world/workflows/main/main.cwl";
assert_eq!(read_node_type(path), FileType::Workflow);
let base = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let main_cwl = base
.join("../../testdata/hello_world/workflows/main/main.cwl")
.canonicalize()
.expect("Test file not found");
assert_eq!(read_node_type(main_cwl), FileType::Workflow);

let path = "../../testdata/hello_world/workflows/calculation/calculation.cwl";
assert_eq!(read_node_type(path), FileType::CommandLineTool);
let calc_cwl = base
.join("../../testdata/hello_world/workflows/calculation/calculation.cwl")
.canonicalize()
.expect("Test file not found");
assert_eq!(read_node_type(calc_cwl), FileType::CommandLineTool);
}
}
160 changes: 152 additions & 8 deletions packages/gui/src/components/files/solution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,40 @@ use crate::components::{ICON_SIZE, SmallRoundActionButton};
use crate::files::{get_cwl_files, get_submodules_cwl_files};
use crate::layout::{INPUT_TEXT_CLASSES, RELOAD_TRIGGER, Route};
use crate::use_app_state;
use crate::reana_integration::{execute_reana_workflow, get_reana_credentials};
use dioxus::prelude::*;
use dioxus_free_icons::Icon;
use dioxus_free_icons::icons::go_icons::{GoCloud, GoFileDirectory, GoPlusCircle, GoTrash};
use dioxus_free_icons::icons::go_icons::{GoCloud, GoFileDirectory, GoPlusCircle, GoTrash, GoPlay};
use repository::Repository;
use repository::submodule::{add_submodule, remove_submodule};
use reqwest::Url;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use commonwl::execution::execute_cwlfile;
use crate::components::files::{FileType, read_node_type};
use crate::components::ExecutionType;
use tokio::sync::mpsc;
use dioxus::core::spawn;

#[component]
pub fn SolutionView(project_path: ReadSignal<PathBuf>, dialog_signals: (Signal<bool>, Signal<bool>)) -> Element {
let mut app_state = use_app_state();

let files = use_memo(move || {
RELOAD_TRIGGER(); //subscribe to changes
RELOAD_TRIGGER();
get_cwl_files(project_path().join("workflows"))
});
let submodule_files = use_memo(move || {
RELOAD_TRIGGER(); //subscribe to changes
RELOAD_TRIGGER();
get_submodules_cwl_files(project_path())
});

let mut hover = use_signal(|| false);
let mut adding = use_signal(|| false);
let mut processing = use_signal(|| false);
let mut new_package = use_signal(String::new);

let show_settings = use_signal(|| false);
rsx! {
div { class: "flex flex-grow flex-col overflow-y-auto",
h2 { class: "mt-2 font-bold flex gap-1 items-center",
Expand Down Expand Up @@ -76,7 +82,7 @@ pub fn SolutionView(project_path: ReadSignal<PathBuf>, dialog_signals: (Signal<b
}
if hover() {
SmallRoundActionButton {
class: "ml-auto mr-3 hover:bg-fairagro-red-light",
class: "hover:bg-fairagro-red-light",
title: "Delete {item.name}",
onclick: {
//we need to double clone here ... ugly :/
Expand Down Expand Up @@ -114,15 +120,128 @@ pub fn SolutionView(project_path: ReadSignal<PathBuf>, dialog_signals: (Signal<b
icon: GoTrash,
}
}
// local
SmallRoundActionButton {
class: "hover:bg-fairagro-mid-500",
title: "Run locally",
onclick: {
let item = item.clone();
let app_state = app_state;
move |_| {
let item = item.clone();
let mut app_state = app_state;
async move {
{
let mut state = app_state.write();
state.active_tab.set("terminal".to_string());
state.show_terminal_log.set(true);
state.terminal_log.set(String::new());
state.terminal_exec_type.set(ExecutionType::Local);
}
let Some(dir) = app_state().working_directory.clone() else {
eprintln!("❌ No working directory");
return Ok(());
};
let args = vec![dir.join("inputs.yml").to_string_lossy().to_string()];
let mut terminal_signal = app_state().terminal_log;
terminal_signal.set("🚀 Starting local execution...\n".to_string());
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(64);
tokio::task::spawn_blocking({
let item = item.clone();
let args = args.clone();
let dir = dir.clone();
let tx = tx.clone();
move || {
let result = resolve_safe_cwl_path(&dir, Path::new(&item.path));
let cwl_path = match result {
Ok(path) => path,
Err(msg) => {
let _ = tx.blocking_send(format!("{msg}\n"));
return;
}
};
let inputs_file = dir.join("inputs.yml");
if !inputs_file.exists() {
let _ = tx.blocking_send(format!("❌ inputs.yml not found: {:?}\n", inputs_file));
return;
}
let result = execute_cwlfile(&cwl_path, &args, Some(dir));
let _ = match result {
Ok(_) => tx.blocking_send("✅ Local execution completed.\n".to_string()),
Err(e) => tx.blocking_send(format!("❌ Execution failed: {e}\n")),
};
}
});
while let Some(line) = rx.recv().await {
terminal_signal.with_mut(|t| t.push_str(&line));
}
Ok(())
}
}
},
Icon { width: 10, height: 10, icon: GoPlay }
}
// REANA
if read_node_type(&item.path) == FileType::Workflow {
SmallRoundActionButton {
class: "hover:bg-fairagro-mid-500",
title: "Execute with REANA".to_string(),
onclick: {
let item = item.clone();
let app_state = app_state;
move |_| {
let item = item.clone();
let show_settings = show_settings;
let mut app_state = app_state;

app_state.write().active_tab.set("terminal".to_string());
app_state.write().show_terminal_log.set(true);
app_state.write().terminal_log.set(String::new());
app_state.write().terminal_exec_type.set(ExecutionType::Remote);

let creds = get_reana_credentials().ok().flatten();
if creds.is_none() {
app_state.write().show_manage_reana_modal.set(true);
return Ok(());
}
let (_instance_url, _token) = creds.unwrap();
let working_dir = match app_state().working_directory.clone() {
Some(dir) => dir,
None => {
eprintln!("❌ No working directory set");
return Ok(());
}
};
let mut terminal_signal = app_state().terminal_log;
let (tx, mut rx) = mpsc::channel::<String>(100);
spawn(async move {
while let Some(msg) = rx.recv().await {
let mut log = terminal_signal();
log.push_str(&msg);
terminal_signal.set(log);
}
});
dioxus::prelude::spawn(async move {
if let Err(e) = execute_reana_workflow(item, working_dir, show_settings, Some(tx)).await {
let mut log = terminal_signal();
log.push_str(&format!("\n❌ Execution failed: {e}\n"));
terminal_signal.set(log);
}
});
Ok(())
}
},
Icon { width: 10, height: 10, icon: GoCloud }
}
}
}
}
}
}
}
for (module , files) in submodule_files() {
Submodule_View { module, files, dialog_signals }
Submodule_View { module, files, dialog_signals }
}
}

h2 {
class: "mt-2 font-bold flex gap-1 items-center cursor-pointer",
onclick: move |_| adding.set(true),
Expand Down Expand Up @@ -253,3 +372,28 @@ pub fn Submodule_View(module: String, files: Vec<Node>, dialog_signals: (Signal<
}
}
}

pub fn resolve_safe_cwl_path(base_dir: &Path, candidate: &Path) -> Result<PathBuf, String> {
let base = base_dir
.canonicalize()
.map_err(|e| format!("Failed to canonicalize base directory {:?}: {e}", base_dir))?;
let joined = if candidate.is_absolute() {
candidate.to_path_buf()
} else {
base.join(candidate)
};
let resolved = joined
.canonicalize()
.map_err(|e| format!("Failed to canonicalize CWL path {:?}: {e}", candidate))?;
if !resolved.starts_with(&base) {
return Err(format!(
"❌ Unsafe CWL path: {:?} is outside the working directory {:?}",
resolved, base
));
}
if !resolved.exists() {
return Err(format!("❌ CWL file not found: {:?}", resolved));
}

Ok(resolved)
}
Loading