wraith/src-tauri/src/commands/docker_commands.rs
Vantz Stockwell 1b74527a62
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 2m53s
feat: tab notifications, session persistence, Docker panel, drag reorder sidebar
Tab activity notifications:
- Background tabs pulse blue when new output arrives
- Clears when you switch to the tab
- useTerminal marks activity on every data event

Session persistence:
- Workspace saved to DB on window close (connection IDs + positions)
- Restored on launch — auto-reconnects saved sessions in order
- workspace_commands: save_workspace, load_workspace

Docker Manager (Tools → Docker Manager):
- Containers tab: list all, start/stop/restart/remove/logs
- Images tab: list all, remove
- Volumes tab: list all, remove
- One-click Builder Prune and System Prune buttons
- All operations via SSH exec channels — no Docker socket exposure

Sidebar drag-and-drop:
- Drag groups to reorder
- Drag connections between groups
- Drag connections within a group to reorder
- Blue border indicator on drop targets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:50:49 -04:00

116 lines
4.9 KiB
Rust

//! Tauri commands for Docker management via SSH exec channels.
use tauri::State;
use serde::Serialize;
use crate::AppState;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DockerContainer {
pub id: String,
pub name: String,
pub image: String,
pub status: String,
pub ports: String,
pub created: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DockerImage {
pub id: String,
pub repository: String,
pub tag: String,
pub size: String,
pub created: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DockerVolume {
pub name: String,
pub driver: String,
pub mountpoint: String,
}
#[tauri::command]
pub async fn docker_list_containers(session_id: String, all: Option<bool>, state: State<'_, AppState>) -> Result<Vec<DockerContainer>, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let flag = if all.unwrap_or(true) { "-a" } else { "" };
let output = exec(&session.handle, &format!("docker ps {} --format '{{{{.ID}}}}|{{{{.Names}}}}|{{{{.Image}}}}|{{{{.Status}}}}|{{{{.Ports}}}}|{{{{.CreatedAt}}}}' 2>&1", flag)).await?;
Ok(output.lines().filter(|l| !l.is_empty() && !l.starts_with("CONTAINER")).map(|line| {
let p: Vec<&str> = line.splitn(6, '|').collect();
DockerContainer {
id: p.first().unwrap_or(&"").to_string(),
name: p.get(1).unwrap_or(&"").to_string(),
image: p.get(2).unwrap_or(&"").to_string(),
status: p.get(3).unwrap_or(&"").to_string(),
ports: p.get(4).unwrap_or(&"").to_string(),
created: p.get(5).unwrap_or(&"").to_string(),
}
}).collect())
}
#[tauri::command]
pub async fn docker_list_images(session_id: String, state: State<'_, AppState>) -> Result<Vec<DockerImage>, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let output = exec(&session.handle, "docker images --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}' 2>&1").await?;
Ok(output.lines().filter(|l| !l.is_empty()).map(|line| {
let p: Vec<&str> = line.splitn(5, '|').collect();
DockerImage {
id: p.first().unwrap_or(&"").to_string(),
repository: p.get(1).unwrap_or(&"").to_string(),
tag: p.get(2).unwrap_or(&"").to_string(),
size: p.get(3).unwrap_or(&"").to_string(),
created: p.get(4).unwrap_or(&"").to_string(),
}
}).collect())
}
#[tauri::command]
pub async fn docker_list_volumes(session_id: String, state: State<'_, AppState>) -> Result<Vec<DockerVolume>, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let output = exec(&session.handle, "docker volume ls --format '{{.Name}}|{{.Driver}}|{{.Mountpoint}}' 2>&1").await?;
Ok(output.lines().filter(|l| !l.is_empty()).map(|line| {
let p: Vec<&str> = line.splitn(3, '|').collect();
DockerVolume {
name: p.first().unwrap_or(&"").to_string(),
driver: p.get(1).unwrap_or(&"").to_string(),
mountpoint: p.get(2).unwrap_or(&"").to_string(),
}
}).collect())
}
#[tauri::command]
pub async fn docker_action(session_id: String, action: String, target: String, state: State<'_, AppState>) -> Result<String, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let cmd = match action.as_str() {
"start" => format!("docker start {} 2>&1", target),
"stop" => format!("docker stop {} 2>&1", target),
"restart" => format!("docker restart {} 2>&1", target),
"remove" => format!("docker rm -f {} 2>&1", target),
"logs" => format!("docker logs --tail 100 {} 2>&1", target),
"remove-image" => format!("docker rmi {} 2>&1", target),
"remove-volume" => format!("docker volume rm {} 2>&1", target),
"builder-prune" => "docker builder prune -f 2>&1".to_string(),
"system-prune" => "docker system prune -f 2>&1".to_string(),
"system-prune-all" => "docker system prune -a -f 2>&1".to_string(),
_ => return Err(format!("Unknown docker action: {}", action)),
};
exec(&session.handle, &cmd).await
}
async fn exec(handle: &std::sync::Arc<tokio::sync::Mutex<russh::client::Handle<crate::ssh::session::SshClient>>>, cmd: &str) -> Result<String, String> {
let mut channel = { let h = handle.lock().await; h.channel_open_session().await.map_err(|e| format!("Exec failed: {}", e))? };
channel.exec(true, cmd).await.map_err(|e| format!("Exec failed: {}", e))?;
let mut output = String::new();
loop {
match channel.wait().await {
Some(russh::ChannelMsg::Data { ref data }) => { if let Ok(t) = std::str::from_utf8(data.as_ref()) { output.push_str(t); } }
Some(russh::ChannelMsg::Eof) | Some(russh::ChannelMsg::Close) | None => break,
_ => {}
}
}
Ok(output)
}