wraith/src/components/tools/DockerPanel.vue
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

114 lines
6.2 KiB
Vue

<template>
<div class="flex flex-col h-full p-4 gap-3">
<!-- Tabs -->
<div class="flex items-center gap-2">
<button v-for="t in ['containers','images','volumes']" :key="t"
class="px-3 py-1 text-xs rounded cursor-pointer transition-colors"
:class="tab === t ? 'bg-[#58a6ff] text-black font-bold' : 'bg-[#21262d] text-[#8b949e] hover:text-white'"
@click="tab = t; refresh()"
>{{ t.charAt(0).toUpperCase() + t.slice(1) }}</button>
<div class="ml-auto flex gap-1">
<button class="px-2 py-1 text-[10px] rounded bg-[#21262d] text-[#8b949e] hover:text-white cursor-pointer" @click="refresh">Refresh</button>
<button class="px-2 py-1 text-[10px] rounded bg-[#da3633] text-white cursor-pointer" @click="action('builder-prune', '')">Builder Prune</button>
<button class="px-2 py-1 text-[10px] rounded bg-[#da3633] text-white cursor-pointer" @click="action('system-prune', '')">System Prune</button>
</div>
</div>
<!-- Containers -->
<div v-if="tab === 'containers'" class="flex-1 overflow-auto border border-[#30363d] rounded">
<table class="w-full text-xs"><thead class="bg-[#161b22] sticky top-0"><tr>
<th class="text-left px-3 py-2 text-[#8b949e]">Name</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Image</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Status</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Actions</th>
</tr></thead><tbody>
<tr v-for="c in containers" :key="c.id" class="border-t border-[#21262d] hover:bg-[#161b22]">
<td class="px-3 py-1.5 font-mono">{{ c.name }}</td>
<td class="px-3 py-1.5 text-[#8b949e]">{{ c.image }}</td>
<td class="px-3 py-1.5" :class="c.status.startsWith('Up') ? 'text-[#3fb950]' : 'text-[#8b949e]'">{{ c.status }}</td>
<td class="px-3 py-1.5 flex gap-1">
<button v-if="!c.status.startsWith('Up')" class="px-1.5 py-0.5 text-[10px] rounded bg-[#238636] text-white cursor-pointer" @click="action('start', c.name)">Start</button>
<button v-if="c.status.startsWith('Up')" class="px-1.5 py-0.5 text-[10px] rounded bg-[#e3b341] text-black cursor-pointer" @click="action('stop', c.name)">Stop</button>
<button class="px-1.5 py-0.5 text-[10px] rounded bg-[#1f6feb] text-white cursor-pointer" @click="action('restart', c.name)">Restart</button>
<button class="px-1.5 py-0.5 text-[10px] rounded bg-[#21262d] text-[#8b949e] cursor-pointer" @click="viewLogs(c.name)">Logs</button>
<button class="px-1.5 py-0.5 text-[10px] rounded bg-[#da3633] text-white cursor-pointer" @click="action('remove', c.name)">Remove</button>
</td>
</tr>
</tbody></table>
</div>
<!-- Images -->
<div v-if="tab === 'images'" class="flex-1 overflow-auto border border-[#30363d] rounded">
<table class="w-full text-xs"><thead class="bg-[#161b22] sticky top-0"><tr>
<th class="text-left px-3 py-2 text-[#8b949e]">Repository</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Tag</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Size</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Actions</th>
</tr></thead><tbody>
<tr v-for="img in images" :key="img.id" class="border-t border-[#21262d] hover:bg-[#161b22]">
<td class="px-3 py-1.5 font-mono">{{ img.repository }}</td>
<td class="px-3 py-1.5">{{ img.tag }}</td>
<td class="px-3 py-1.5 text-[#8b949e]">{{ img.size }}</td>
<td class="px-3 py-1.5"><button class="px-1.5 py-0.5 text-[10px] rounded bg-[#da3633] text-white cursor-pointer" @click="action('remove-image', img.id)">Remove</button></td>
</tr>
</tbody></table>
</div>
<!-- Volumes -->
<div v-if="tab === 'volumes'" class="flex-1 overflow-auto border border-[#30363d] rounded">
<table class="w-full text-xs"><thead class="bg-[#161b22] sticky top-0"><tr>
<th class="text-left px-3 py-2 text-[#8b949e]">Name</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Driver</th>
<th class="text-left px-3 py-2 text-[#8b949e]">Actions</th>
</tr></thead><tbody>
<tr v-for="v in volumes" :key="v.name" class="border-t border-[#21262d] hover:bg-[#161b22]">
<td class="px-3 py-1.5 font-mono">{{ v.name }}</td>
<td class="px-3 py-1.5 text-[#8b949e]">{{ v.driver }}</td>
<td class="px-3 py-1.5"><button class="px-1.5 py-0.5 text-[10px] rounded bg-[#da3633] text-white cursor-pointer" @click="action('remove-volume', v.name)">Remove</button></td>
</tr>
</tbody></table>
</div>
<!-- Output -->
<pre v-if="output" class="max-h-32 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-2 text-[10px] font-mono text-[#e0e0e0]">{{ output }}</pre>
<div class="text-[10px] text-[#484f58]">{{ containers.length }} containers · {{ images.length }} images · {{ volumes.length }} volumes</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
const props = defineProps<{ sessionId: string }>();
const tab = ref("containers");
const containers = ref<any[]>([]);
const images = ref<any[]>([]);
const volumes = ref<any[]>([]);
const output = ref("");
async function refresh(): Promise<void> {
try {
if (tab.value === "containers") containers.value = await invoke("docker_list_containers", { sessionId: props.sessionId, all: true });
if (tab.value === "images") images.value = await invoke("docker_list_images", { sessionId: props.sessionId });
if (tab.value === "volumes") volumes.value = await invoke("docker_list_volumes", { sessionId: props.sessionId });
} catch (err) { output.value = String(err); }
}
async function action(act: string, target: string): Promise<void> {
try {
output.value = await invoke<string>("docker_action", { sessionId: props.sessionId, action: act, target });
await refresh();
} catch (err) { output.value = String(err); }
}
async function viewLogs(name: string): Promise<void> {
try { output.value = await invoke<string>("docker_action", { sessionId: props.sessionId, action: "logs", target: name }); }
catch (err) { output.value = String(err); }
}
onMounted(refresh);
</script>