Skip to content
Merged
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
23 changes: 23 additions & 0 deletions assets/sensible-db-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions assets/sensible-db-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
588 changes: 588 additions & 0 deletions docs/design/explorer-redesign.md

Large diffs are not rendered by default.

116 changes: 94 additions & 22 deletions nexus-explorer/src/commands/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,15 @@ pub fn db_close(state: tauri::State<AppState>, name: String) -> Result<String, S
#[tauri::command]
pub fn db_list(state: tauri::State<AppState>) -> Result<Vec<String>, String> {
let dbs = state.databases.lock().map_err(|e| e.to_string())?;
Ok(dbs.keys().cloned().collect())
let keys: Vec<String> = dbs.keys().cloned().collect();
let msg = format!("[DB_LIST] Returning {} databases: {:?}
", keys.len(), keys);
eprint!("{}", msg);
std::fs::write("/tmp/nexus-explorer.log", format!("{}
[DB_LIST] Returning {} databases: {:?}
",
std::fs::read_to_string("/tmp/nexus-explorer.log").unwrap_or_default(), keys.len(), keys)).ok();
Ok(keys)
}

#[tauri::command]
Expand All @@ -76,39 +84,38 @@ pub fn db_stats(state: tauri::State<AppState>, name: String) -> Result<DbStats,
pub fn db_create_demo_internal(state: &AppState) -> Result<(), String> {
use nexus_db::embedded::database::Database;

let demo_path = dirs::home_dir()
let nexus_path = dirs::home_dir()
.ok_or("Could not determine home directory")?
.join(".nexus")
.join("demo");
.join(".nexus");

if demo_path.exists() {
let dbs = state.databases.lock().map_err(|e| e.to_string())?;
if dbs.contains_key("demo") {
return Ok(());
}
drop(dbs);

let db = Database::open(&demo_path).map_err(|e| e.to_string())?;
let mut dbs = state.databases.lock().map_err(|e| e.to_string())?;
dbs.insert("demo".to_string(), Arc::new(db));
return Ok(());
// Demo 1: Health Patterns
let health_path = nexus_path.join("health-patterns");
if !health_path.exists() {
std::fs::create_dir_all(&health_path).map_err(|e| e.to_string())?;
}

let db = Database::open(&demo_path).map_err(|e| e.to_string())?;
populate_demo_data(&db)?;

let health_db = Database::open(&health_path).map_err(|e| e.to_string())?;
populate_health_patterns(&health_db)?;
let mut dbs = state.databases.lock().map_err(|e| e.to_string())?;
dbs.insert("demo".to_string(), Arc::new(db));
dbs.insert("health-patterns".to_string(), Arc::new(health_db));

// Demo 2: Project Management
let project_path = nexus_path.join("project-management");
if !project_path.exists() {
std::fs::create_dir_all(&project_path).map_err(|e| e.to_string())?;
}
let project_db = Database::open(&project_path).map_err(|e| e.to_string())?;
populate_project_management(&project_db)?;
dbs.insert("project-management".to_string(), Arc::new(project_db));

Ok(())
}

#[tauri::command]
pub fn db_create_demo(state: tauri::State<AppState>) -> Result<String, String> {
db_create_demo_internal(&state).map(|_| "Demo database ready".to_string())
db_create_demo_internal(&state).map(|_| "Demo databases ready: health-patterns, project-management".to_string())
}

fn populate_demo_data(db: &nexus_db::embedded::database::Database) -> Result<(), String> {
fn populate_health_patterns(db: &nexus_db::embedded::database::Database) -> Result<(), String> {
db.put_node(Node {
id: 1,
label: "Person:Alex".to_string(),
Expand Down Expand Up @@ -280,3 +287,68 @@ fn populate_demo_data(db: &nexus_db::embedded::database::Database) -> Result<(),

Ok(())
}

#[tauri::command]
pub fn log_error(msg: String) {
eprintln!("[FRONTEND_ERROR] {}", msg);
let _ = std::fs::write("/tmp/nexus-explorer.log", format!("{}
[FRONTEND_ERROR] {}
",
std::fs::read_to_string("/tmp/nexus-explorer.log").unwrap_or_default(), msg));
}

fn populate_project_management(db: &nexus_db::embedded::database::Database) -> Result<(), String> {
// Team Members
db.put_node(Node { id: 1, label: "Person:Alice".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 2, label: "Person:Bob".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 3, label: "Person:Carol".to_string() }).map_err(|e| e.to_string())?;

// Projects
db.put_node(Node { id: 10, label: "Project:WebsiteRedesign".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 11, label: "Project:MobileApp".to_string() }).map_err(|e| e.to_string())?;

// Tasks
db.put_node(Node { id: 20, label: "Task:DesignMockups".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 21, label: "Task:FrontendDev".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 22, label: "Task:BackendAPI".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 23, label: "Task:Testing".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 24, label: "Task:Deployment".to_string() }).map_err(|e| e.to_string())?;

// Tools
db.put_node(Node { id: 30, label: "Tool:Figma".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 31, label: "Tool:GitHub".to_string() }).map_err(|e| e.to_string())?;
db.put_node(Node { id: 32, label: "Tool:AWS".to_string() }).map_err(|e| e.to_string())?;

// Assignments: Person -> Project
db.put_edge(Edge { id: 100, label: "ASSIGNED_TO".to_string(), from: 1, to: 10 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 101, label: "ASSIGNED_TO".to_string(), from: 2, to: 10 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 102, label: "ASSIGNED_TO".to_string(), from: 3, to: 11 }).map_err(|e| e.to_string())?;

// Tasks belong to projects
db.put_edge(Edge { id: 110, label: "PART_OF".to_string(), from: 20, to: 10 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 111, label: "PART_OF".to_string(), from: 21, to: 10 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 112, label: "PART_OF".to_string(), from: 22, to: 11 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 113, label: "PART_OF".to_string(), from: 23, to: 11 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 114, label: "PART_OF".to_string(), from: 24, to: 11 }).map_err(|e| e.to_string())?;

// Task dependencies
db.put_edge(Edge { id: 120, label: "BLOCKS".to_string(), from: 20, to: 21 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 121, label: "BLOCKS".to_string(), from: 21, to: 23 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 122, label: "BLOCKS".to_string(), from: 22, to: 23 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 123, label: "BLOCKS".to_string(), from: 23, to: 24 }).map_err(|e| e.to_string())?;

// Tasks use tools
db.put_edge(Edge { id: 130, label: "USES".to_string(), from: 20, to: 30 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 131, label: "USES".to_string(), from: 21, to: 31 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 132, label: "USES".to_string(), from: 22, to: 31 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 133, label: "USES".to_string(), from: 24, to: 32 }).map_err(|e| e.to_string())?;

// Person owns tasks
db.put_edge(Edge { id: 140, label: "OWNS".to_string(), from: 1, to: 20 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 141, label: "OWNS".to_string(), from: 2, to: 21 }).map_err(|e| e.to_string())?;
db.put_edge(Edge { id: 142, label: "OWNS".to_string(), from: 3, to: 22 }).map_err(|e| e.to_string())?;

Ok(())
}


15 changes: 14 additions & 1 deletion nexus-explorer/src/commands/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,25 @@ pub fn node_delete(

#[tauri::command]
pub fn node_list(state: tauri::State<AppState>, db_name: String) -> Result<Vec<NodeDto>, String> {
let log_msg = format!("[NODE_LIST] Called with db_name={}
", db_name);
eprint!("{}", log_msg);
let _ = std::fs::write("/tmp/nexus-explorer.log", format!("{}
{}",
std::fs::read_to_string("/tmp/nexus-explorer.log").unwrap_or_default(), log_msg));
let dbs = state.databases.lock().map_err(|e| e.to_string())?;
eprintln!("[NODE_LIST] Available DBs: {:?}", dbs.keys().collect::<Vec<_>>());
let db = dbs
.get(&db_name)
.ok_or_else(|| format!("Database '{}' not found", db_name))?;
.ok_or_else(|| format!("Database '{}' not found. Available: {:?}", db_name, dbs.keys().collect::<Vec<_>>()))?;
let tx = db.read_transaction().map_err(|e| e.to_string())?;
let nodes = tx.scan_nodes().map_err(|e| e.to_string())?;
let log_msg2 = format!("[NODE_LIST] db={} found {} nodes
", db_name, nodes.len());
eprint!("{}", log_msg2);
let _ = std::fs::write("/tmp/nexus-explorer.log", format!("{}
{}",
std::fs::read_to_string("/tmp/nexus-explorer.log").unwrap_or_default(), log_msg2));
Ok(nodes
.into_iter()
.map(|n| NodeDto {
Expand Down
30 changes: 22 additions & 8 deletions nexus-explorer/src/commands/nql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct QResult {
count: usize,
}

#[tauri::command]
#[tauri::command(rename_all = "camelCase")]
pub fn nql_execute(
state: tauri::State<AppState>,
db_name: String,
Expand Down Expand Up @@ -77,15 +77,15 @@ pub fn nql_execute(
fn do_match(q: &str, nodes: &[Node], edges: &[Edge]) -> Result<QResult, String> {
let label = pick_label(q);
let matched: Vec<QNode> = nodes.iter()
.filter(|n| label.as_ref().map_or(true, |l| n.label.contains(l)))
.filter(|n| label.as_ref().map_or(true, |l| n.label.to_lowercase().contains(l)))
.map(|n| QNode { id: n.id, label: n.label.clone() })
.collect();
let matched_edges: Vec<QEdge> = if q.contains(")-[") || q.contains("]->") {
let el = pick_edge_label(q);
edges.iter()
.filter(|e| {
let nm = matched.iter().any(|n| n.id == e.from || n.id == e.to);
el.as_ref().map_or(nm, |x| e.label.contains(x) && nm)
el.as_ref().map_or(nm, |x| e.label.to_lowercase().contains(x) && nm)
})
.map(|e| QEdge { id: e.id, label: e.label.clone(), from: e.from, to: e.to })
.collect()
Expand All @@ -111,16 +111,30 @@ fn do_count(q: &str, nodes: &[Node], edges: &[Edge]) -> Result<QResult, String>
let count = if q.contains("edge") || q.contains("relationship") {
edges.len()
} else {
nodes.iter().filter(|n| label.as_ref().map_or(true, |l| n.label.contains(l))).count()
nodes.iter().filter(|n| label.as_ref().map_or(true, |l| n.label.to_lowercase().contains(l))).count()
};
Ok(QResult { nodes: vec![], edges: vec![], count })
}

fn pick_label(q: &str) -> Option<String> {
let pos = q.find(':')?;
let rest = &q[pos + 1..];
let end = rest.find(|c: char| !c.is_alphanumeric() && c != '_').unwrap_or(rest.len());
if end > 0 { Some(rest[..end].to_string()) } else { None }
// Only match node labels like (n:Label), not edge labels [r:Label]
// Look for pattern (X:Label) where X is a single letter
for (i, _) in q.match_indices(':') {
// Check if this is a node label: look back for '('
let before = &q[..i];
if let Some(paren) = before.rfind('(') {
let between = &before[paren + 1..];
// Node pattern: single letter variable like (n:Label)
if between.len() <= 2 && between.chars().all(|c| c.is_alphabetic()) {
let after = &q[i + 1..];
let end = after.find(|c: char| !c.is_alphanumeric() && c != '_').unwrap_or(after.len());
if end > 0 {
return Some(after[..end].to_string());
}
}
}
}
None
}

fn pick_edge_label(q: &str) -> Option<String> {
Expand Down
Loading
Loading