feat: add projects groups

This commit is contained in:
2026-05-19 16:39:35 +02:00
parent ba4de62a34
commit d1db386e1d
2 changed files with 535 additions and 134 deletions

View File

@ -4,25 +4,42 @@
let {
agentId,
containerId,
containerName,
containerId = '',
containerName = '',
containers = [] as Array<{id: string; name: string}>,
projectName = '',
onClose,
}: {
agentId: string;
containerId: string;
containerName: string;
containerId?: string;
containerName?: string;
containers?: Array<{id: string; name: string}>;
projectName?: string;
onClose: () => void;
} = $props();
interface LogLine {
stream: string;
line: string;
prefix?: string;
prefixColor?: string;
}
const PREFIX_COLORS = [
'text-cyan-400',
'text-green-400',
'text-yellow-400',
'text-orange-400',
'text-pink-400',
'text-violet-400',
'text-sky-400',
'text-lime-400',
];
let lines = $state<LogLine[]>([]);
let autoScroll = $state(true);
let logEl = $state<HTMLElement | null>(null);
let disconnect: (() => void) | null = null;
let disconnects: Array<() => void> = [];
function stripAnsi(str: string): string {
// eslint-disable-next-line no-control-regex
@ -41,17 +58,38 @@
autoScroll = atBottom;
}
const isMulti = $derived(containers.length > 0);
const title = $derived(isMulti ? projectName : containerName);
onMount(() => {
disconnect = connectLogs(agentId, containerId, async (msg) => {
if (msg.line) {
lines.push({ stream: msg.stream, line: stripAnsi(msg.line) });
if (lines.length > 2000) lines = lines.slice(-2000);
await scrollToBottom();
}
});
if (isMulti) {
containers.forEach((c, idx) => {
const prefix = c.name;
const prefixColor = PREFIX_COLORS[idx % PREFIX_COLORS.length];
const dc = connectLogs(agentId, c.id, async (msg) => {
if (msg.line) {
lines.push({ stream: msg.stream, line: stripAnsi(msg.line), prefix, prefixColor });
if (lines.length > 2000) lines = lines.slice(-2000);
await scrollToBottom();
}
});
disconnects.push(dc);
});
} else {
const dc = connectLogs(agentId, containerId, async (msg) => {
if (msg.line) {
lines.push({ stream: msg.stream, line: stripAnsi(msg.line) });
if (lines.length > 2000) lines = lines.slice(-2000);
await scrollToBottom();
}
});
disconnects.push(dc);
}
});
onDestroy(() => disconnect?.());
onDestroy(() => {
for (const dc of disconnects) dc();
});
function handleKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
@ -65,7 +103,7 @@
class="fixed inset-0 z-50 flex flex-col bg-black/70 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-label="Logs — {containerName}"
aria-label="Logs — {title}"
>
<!-- Modal panel -->
<div class="flex flex-col m-4 md:m-8 flex-1 min-h-0 card overflow-hidden">
@ -73,7 +111,7 @@
<!-- Header -->
<div class="flex items-center gap-3 px-4 py-3 border-b border-white/[0.07] shrink-0">
<span class="w-2 h-2 rounded-full bg-signal-green animate-pulse"></span>
<span class="font-mono text-sm font-semibold text-slate-200">{containerName}</span>
<span class="font-mono text-sm font-semibold text-slate-200">{title}</span>
<span class="text-xs text-slate-600 ml-1">logs</span>
<div class="ml-auto flex items-center gap-3">
@ -109,9 +147,12 @@
{#if lines.length === 0}
<p class="text-slate-700 italic">En attente des logs…</p>
{:else}
{#each lines as { stream, line }, i (i)}
<div class="whitespace-pre-wrap break-all {stream === 'stderr' ? 'text-signal-red/80' : 'text-slate-300'}">
{line}
{#each lines as { stream, line, prefix, prefixColor }, i (i)}
<div class="whitespace-pre-wrap break-all flex {stream === 'stderr' ? 'text-signal-red/80' : 'text-slate-300'}">
{#if prefix}
<span class="{prefixColor} mr-1 opacity-80 shrink-0">[{prefix}]</span>
{/if}
<span>{line}</span>
</div>
{/each}
{/if}