Skip to content

Commit 5ca0628

Browse files
authored
Merge pull request #217 from OpenMined/madhava/facet-subselect
Madhava/facet subselect
2 parents c1911bb + 40125c4 commit 5ca0628

8 files changed

Lines changed: 360 additions & 106 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ on:
2121
build_macos_x64:
2222
description: "Build macOS (Intel)"
2323
required: false
24-
default: false
24+
default: true
2525
type: boolean
2626
build_windows_x64:
2727
description: "Build Windows (x86_64)"
@@ -31,12 +31,12 @@ on:
3131
build_linux_x64:
3232
description: "Build Linux (x86_64)"
3333
required: false
34-
default: false
34+
default: true
3535
type: boolean
3636
build_linux_arm64:
3737
description: "Build Linux (arm64, no bundled deps)"
3838
required: false
39-
default: false
39+
default: true
4040
type: boolean
4141
workflow_call:
4242
inputs:
@@ -54,19 +54,19 @@ on:
5454
build_macos_x64:
5555
required: false
5656
type: boolean
57-
default: false
57+
default: true
5858
build_windows_x64:
5959
required: false
6060
type: boolean
6161
default: true
6262
build_linux_x64:
6363
required: false
6464
type: boolean
65-
default: false
65+
default: true
6666
build_linux_arm64:
6767
required: false
6868
type: boolean
69-
default: false
69+
default: true
7070

7171
concurrency:
7272
group: release-${{ github.ref_name }}

src-tauri/src/commands/files/crud.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ fn sources_for_file_ids(
9191
.map_err(|e| format!("Failed to prepare source lookup: {}", e))?;
9292

9393
let rows = stmt
94-
.query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)))
94+
.query_map([], |row| {
95+
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
96+
})
9597
.map_err(|e| format!("Failed to read source lookup: {}", e))?;
9698
for row in rows {
9799
let (file_id, source) = row.map_err(|e| format!("Failed to collect sources: {}", e))?;

src-tauri/src/commands/files/facets.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,12 @@ pub fn import_participant_facets(
239239
) {
240240
Ok(id) => id,
241241
Err(rusqlite::Error::QueryReturnedNoRows) => continue,
242-
Err(e) => return Err(format!("Failed to look up participant {}: {}", participant, e)),
242+
Err(e) => {
243+
return Err(format!(
244+
"Failed to look up participant {}: {}",
245+
participant, e
246+
))
247+
}
243248
};
244249

245250
tx.execute(

src-tauri/src/commands/flows.rs

Lines changed: 148 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,100 @@ fn copy_local_flow_dir(src: &Path, dest: &Path) -> Result<(), String> {
442442
Ok(())
443443
}
444444

445+
fn remove_existing_path(path: &Path, label: &str) -> Result<(), String> {
446+
let metadata = match fs::symlink_metadata(path) {
447+
Ok(metadata) => metadata,
448+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
449+
Err(e) => {
450+
return Err(format!(
451+
"Failed to inspect {} {}: {}",
452+
label,
453+
path.display(),
454+
e
455+
))
456+
}
457+
};
458+
459+
let file_type = metadata.file_type();
460+
if file_type.is_dir() && !file_type.is_symlink() {
461+
fs::remove_dir_all(path)
462+
} else {
463+
fs::remove_file(path)
464+
}
465+
.map_err(|e| {
466+
format!(
467+
"Failed to remove existing {} {}: {}",
468+
label,
469+
path.display(),
470+
e
471+
)
472+
})
473+
}
474+
475+
fn path_exists_or_symlink(path: &Path) -> bool {
476+
fs::symlink_metadata(path).is_ok()
477+
}
478+
479+
fn path_has_wildcard(path: &str) -> bool {
480+
path.contains('*') || path.contains('?')
481+
}
482+
483+
fn wildcard_match(pattern: &str, value: &str) -> bool {
484+
let pattern_chars: Vec<char> = pattern.chars().collect();
485+
let value_chars: Vec<char> = value.chars().collect();
486+
let mut pattern_idx = 0;
487+
let mut value_idx = 0;
488+
let mut star_idx: Option<usize> = None;
489+
let mut star_value_idx = 0;
490+
491+
while value_idx < value_chars.len() {
492+
if pattern_idx < pattern_chars.len()
493+
&& (pattern_chars[pattern_idx] == '?'
494+
|| pattern_chars[pattern_idx] == value_chars[value_idx])
495+
{
496+
pattern_idx += 1;
497+
value_idx += 1;
498+
} else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
499+
star_idx = Some(pattern_idx);
500+
pattern_idx += 1;
501+
star_value_idx = value_idx;
502+
} else if let Some(star) = star_idx {
503+
pattern_idx = star + 1;
504+
star_value_idx += 1;
505+
value_idx = star_value_idx;
506+
} else {
507+
return false;
508+
}
509+
}
510+
511+
while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
512+
pattern_idx += 1;
513+
}
514+
515+
pattern_idx == pattern_chars.len()
516+
}
517+
518+
fn normalized_path_string(path: &Path) -> String {
519+
path.to_string_lossy().replace('\\', "/")
520+
}
521+
522+
fn wildcard_search_root(path: &Path) -> PathBuf {
523+
let mut root = PathBuf::new();
524+
for component in path.components() {
525+
let component_text = component.as_os_str().to_string_lossy();
526+
if path_has_wildcard(&component_text) {
527+
break;
528+
}
529+
root.push(component.as_os_str());
530+
}
531+
532+
if root.as_os_str().is_empty() {
533+
PathBuf::from(".")
534+
} else {
535+
root
536+
}
537+
}
538+
445539
fn list_nextflow_locks(flow_path: &Path) -> Vec<PathBuf> {
446540
let nextflow_dir = flow_path.join(".nextflow");
447541
if !nextflow_dir.exists() {
@@ -1609,10 +1703,9 @@ pub async fn create_flow(
16091703
// Create flow directory in managed location
16101704
let managed_flow_dir = flows_dir.join(&name);
16111705

1612-
if managed_flow_dir.exists() {
1706+
if path_exists_or_symlink(&managed_flow_dir) {
16131707
if overwrite {
1614-
fs::remove_dir_all(&managed_flow_dir)
1615-
.map_err(|e| format!("Failed to remove existing flow directory: {}", e))?;
1708+
remove_existing_path(&managed_flow_dir, "flow directory")?;
16161709
} else {
16171710
return Err(format!(
16181711
"Flow '{}' already exists at {}. Use overwrite to replace.",
@@ -1649,15 +1742,9 @@ pub async fn create_flow(
16491742
let module_dir_name = entry.file_name().to_string_lossy().to_string();
16501743
let dest_module_dir = modules_dir.join(&module_dir_name);
16511744

1652-
if dest_module_dir.exists() {
1745+
if path_exists_or_symlink(&dest_module_dir) {
16531746
if overwrite {
1654-
fs::remove_dir_all(&dest_module_dir).map_err(|e| {
1655-
format!(
1656-
"Failed to remove existing module directory {}: {}",
1657-
dest_module_dir.display(),
1658-
e
1659-
)
1660-
})?;
1747+
remove_existing_path(&dest_module_dir, "module directory")?;
16611748
} else {
16621749
continue;
16631750
}
@@ -1897,9 +1984,8 @@ pub async fn import_flow_from_json(
18971984
.map_err(|e| e.to_string())?;
18981985
}
18991986

1900-
if flow_dir.exists() && overwrite {
1901-
fs::remove_dir_all(&flow_dir)
1902-
.map_err(|e| format!("Failed to remove existing flow directory: {}", e))?;
1987+
if overwrite {
1988+
remove_existing_path(&flow_dir, "flow directory")?;
19031989
}
19041990

19051991
fs::create_dir_all(&flow_dir).map_err(|e| format!("Failed to create flow directory: {}", e))?;
@@ -2068,9 +2154,8 @@ pub async fn delete_flow(state: tauri::State<'_, AppState>, flow_id: i64) -> Res
20682154
let path_buf = PathBuf::from(p.flow_path);
20692155

20702156
// Only delete if the path is within the flows directory
2071-
if path_buf.starts_with(&flows_dir) && path_buf.exists() {
2072-
fs::remove_dir_all(&path_buf)
2073-
.map_err(|e| format!("Failed to delete flow directory: {}", e))?;
2157+
if path_buf.starts_with(&flows_dir) && path_exists_or_symlink(&path_buf) {
2158+
remove_existing_path(&path_buf, "flow directory")?;
20742159
}
20752160
}
20762161

@@ -3873,9 +3958,49 @@ pub fn get_flow_run_logs_full(
38733958

38743959
#[tauri::command]
38753960
pub fn path_exists(path: String) -> Result<bool, String> {
3961+
if path_has_wildcard(&path) {
3962+
return Ok(!resolve_path_matches(path)?.is_empty());
3963+
}
38763964
Ok(Path::new(&path).exists())
38773965
}
38783966

3967+
#[tauri::command]
3968+
pub fn resolve_path_matches(path: String) -> Result<Vec<String>, String> {
3969+
if !path_has_wildcard(&path) {
3970+
let path_buf = PathBuf::from(&path);
3971+
return Ok(if path_buf.exists() {
3972+
vec![path]
3973+
} else {
3974+
Vec::new()
3975+
});
3976+
}
3977+
3978+
let pattern_path = PathBuf::from(&path);
3979+
let search_root = wildcard_search_root(&pattern_path);
3980+
if !search_root.exists() {
3981+
return Ok(Vec::new());
3982+
}
3983+
3984+
let normalized_pattern = path.replace('\\', "/");
3985+
let mut matches = Vec::new();
3986+
for entry in WalkDir::new(&search_root)
3987+
.follow_links(false)
3988+
.into_iter()
3989+
.filter_map(Result::ok)
3990+
{
3991+
if !entry.file_type().is_file() {
3992+
continue;
3993+
}
3994+
let candidate = normalized_path_string(entry.path());
3995+
if wildcard_match(&normalized_pattern, &candidate) {
3996+
matches.push(entry.path().to_string_lossy().to_string());
3997+
}
3998+
}
3999+
matches.sort();
4000+
matches.dedup();
4001+
Ok(matches)
4002+
}
4003+
38794004
#[tauri::command]
38804005
pub fn get_flow_run_work_dir(state: tauri::State<AppState>, run_id: i64) -> Result<String, String> {
38814006
let biovault_db = state.biovault_db.lock().map_err(|e| e.to_string())?;
@@ -3999,11 +4124,10 @@ pub async fn import_flow_from_message(
39994124
let flow_dir = flows_dir.join(&name);
40004125

40014126
// Check if flow already exists
4002-
if flow_dir.exists() {
4127+
if path_exists_or_symlink(&flow_dir) {
40034128
// For now, we'll overwrite - in the future could prompt user
40044129
// or rename with version suffix
4005-
fs::remove_dir_all(&flow_dir)
4006-
.map_err(|e| format!("Failed to remove existing flow: {}", e))?;
4130+
remove_existing_path(&flow_dir, "flow directory")?;
40074131
}
40084132

40094133
// Create flow directory
@@ -4170,10 +4294,9 @@ pub async fn import_flow_from_request(
41704294
.map_err(|e| e.to_string())?;
41714295
}
41724296

4173-
if dest_dir.exists() {
4297+
if path_exists_or_symlink(&dest_dir) {
41744298
if overwrite {
4175-
fs::remove_dir_all(&dest_dir)
4176-
.map_err(|e| format!("Failed to remove existing flow: {}", e))?;
4299+
remove_existing_path(&dest_dir, "flow directory")?;
41774300
} else {
41784301
return Err(format!(
41794302
"Flow '{}' already exists at {}. Use overwrite to replace.",
@@ -4203,15 +4326,9 @@ pub async fn import_flow_from_request(
42034326
let module_dir_name = entry.file_name().to_string_lossy().to_string();
42044327
let dest_module_dir = modules_dir.join(&module_dir_name);
42054328

4206-
if dest_module_dir.exists() {
4329+
if path_exists_or_symlink(&dest_module_dir) {
42074330
if overwrite {
4208-
fs::remove_dir_all(&dest_module_dir).map_err(|e| {
4209-
format!(
4210-
"Failed to remove existing module directory {}: {}",
4211-
dest_module_dir.display(),
4212-
e
4213-
)
4214-
})?;
4331+
remove_existing_path(&dest_module_dir, "module directory")?;
42154332
} else {
42164333
continue;
42174334
}

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,6 +1749,7 @@ pub fn run() {
17491749
resume_flow_run,
17501750
cleanup_flow_run_state,
17511751
path_exists,
1752+
resolve_path_matches,
17521753
delete_flow_run,
17531754
preview_flow_spec,
17541755
import_flow_from_message,

src-tauri/src/ws_bridge.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ fn get_commands_list() -> serde_json::Value {
452452
cmd("cleanup_flow_run_state", "flows", true),
453453
cmd("get_flow_run_work_dir", "flows", true),
454454
cmd("path_exists", "flows", true),
455+
cmd("resolve_path_matches", "flows", true),
455456
cmd("start_analysis", "runs", false),
456457
cmd_async("execute_analysis", "runs", false),
457458
// Sessions
@@ -1345,6 +1346,17 @@ async fn execute_command(app: &AppHandle, cmd: &str, args: Value) -> Result<Valu
13451346
let result = crate::commands::flows::path_exists(path).map_err(|e| e.to_string())?;
13461347
Ok(serde_json::to_value(result).unwrap())
13471348
}
1349+
"resolve_path_matches" => {
1350+
let path: String = serde_json::from_value(
1351+
args.get("path")
1352+
.cloned()
1353+
.ok_or_else(|| "Missing path".to_string())?,
1354+
)
1355+
.map_err(|e| format!("Failed to parse path: {}", e))?;
1356+
let result =
1357+
crate::commands::flows::resolve_path_matches(path).map_err(|e| e.to_string())?;
1358+
Ok(serde_json::to_value(result).unwrap())
1359+
}
13481360
"get_flow_run_logs" => {
13491361
let run_id: i64 = serde_json::from_value(
13501362
args.get("runId")

src/data.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,9 @@ export function createDataModule({ invoke, dialog, getCurrentUserEmail }) {
911911
function pruneSourceFilter(options) {
912912
if (!activeSourceFilters) return
913913
const validValues = new Set(options.map(([value]) => value))
914-
const selected = new Set(Array.from(activeSourceFilters).filter((value) => validValues.has(value)))
914+
const selected = new Set(
915+
Array.from(activeSourceFilters).filter((value) => validValues.has(value)),
916+
)
915917
if (selected.size === validValues.size) {
916918
activeSourceFilters = null
917919
} else {
@@ -940,9 +942,7 @@ export function createDataModule({ invoke, dialog, getCurrentUserEmail }) {
940942
const selectedValues = getSelectedSourceValues(options)
941943
const selectedCount = options.filter(([value]) => selectedValues.has(value)).length
942944
const allChecked = selectedCount === options.length
943-
const summaryLabel = allChecked
944-
? 'Source: All'
945-
: `Source: ${selectedCount}/${options.length}`
945+
const summaryLabel = allChecked ? 'Source: All' : `Source: ${selectedCount}/${options.length}`
946946
const optionItems = options
947947
.map(([value, count]) => {
948948
const inputId = `data-source-filter-${value}`.replace(/[^a-zA-Z0-9_-]/g, '_')

0 commit comments

Comments
 (0)