All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 2m53s
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>
114 lines
6.2 KiB
Vue
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>
|