0.34.1: populate the log container filter (parse compose prefix)
The whole-stack log stream sends service:null on every line, so the container filter dropdown only ever showed "All containers". docker compose logs already prefixes each line with the container name (and an RFC3339 timestamp via --timestamps); parse that prefix client-side to recover the container, populate the filter, and render time + container + message separately (cleaner than the raw prefixed line). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
efb468560e
commit
844655d1c8
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "stackpilot-frontend",
|
"name": "stackpilot-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.34.0",
|
"version": "0.34.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -48,7 +48,44 @@ const levelStyles: Record<Level, { text: string; row: string }> = {
|
|||||||
debug: { text: "text-slate-400", row: "border-l-2 border-transparent" },
|
debug: { text: "text-slate-400", row: "border-l-2 border-transparent" },
|
||||||
};
|
};
|
||||||
|
|
||||||
type LogLine = { service: string | null; line: string; level: Level };
|
// `docker compose logs` prefixes every line with the container name (padded
|
||||||
|
// for alignment) then `| `; with --timestamps an RFC3339 stamp follows. The
|
||||||
|
// whole-stack stream sends service:null, so we recover the container here.
|
||||||
|
const prefixRe = /^(\S+)\s+\|\s?(.*)$/s;
|
||||||
|
const tsRe = /^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s(.*)$/s;
|
||||||
|
|
||||||
|
function parseLine(
|
||||||
|
rawService: string | null,
|
||||||
|
raw: string
|
||||||
|
): { service: string | null; time: string | null; msg: string } {
|
||||||
|
let service = rawService;
|
||||||
|
let body = raw;
|
||||||
|
if (!service) {
|
||||||
|
const m = prefixRe.exec(raw);
|
||||||
|
if (m) {
|
||||||
|
service = m[1];
|
||||||
|
body = m[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let time: string | null = null;
|
||||||
|
const t = tsRe.exec(body);
|
||||||
|
if (t) {
|
||||||
|
const d = new Date(t[1]);
|
||||||
|
if (!Number.isNaN(d.getTime())) {
|
||||||
|
time = d.toLocaleTimeString();
|
||||||
|
body = t[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { service, time, msg: body };
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogLine = {
|
||||||
|
service: string | null;
|
||||||
|
time: string | null;
|
||||||
|
msg: string;
|
||||||
|
raw: string;
|
||||||
|
level: Level;
|
||||||
|
};
|
||||||
|
|
||||||
export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: number }) {
|
export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: number }) {
|
||||||
const [lines, setLines] = useState<LogLine[]>([]);
|
const [lines, setLines] = useState<LogLine[]>([]);
|
||||||
@@ -83,10 +120,11 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num
|
|||||||
try {
|
try {
|
||||||
const msg = JSON.parse(ev.data);
|
const msg = JSON.parse(ev.data);
|
||||||
if (msg.type === "log") {
|
if (msg.type === "log") {
|
||||||
|
const { service, time, msg: body } = parseLine(msg.service, msg.line);
|
||||||
setLines((prev) => {
|
setLines((prev) => {
|
||||||
const next = [
|
const next = [
|
||||||
...prev,
|
...prev,
|
||||||
{ service: msg.service, line: msg.line, level: levelFor(msg.line) },
|
{ service, time, msg: body, raw: msg.line, level: levelFor(body) },
|
||||||
];
|
];
|
||||||
return next.length > MAX_LINES ? next.slice(-MAX_LINES) : next;
|
return next.length > MAX_LINES ? next.slice(-MAX_LINES) : next;
|
||||||
});
|
});
|
||||||
@@ -112,7 +150,7 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num
|
|||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
return lines.filter((l) => {
|
return lines.filter((l) => {
|
||||||
if (serviceFilter !== "__all__" && l.service !== serviceFilter) return false;
|
if (serviceFilter !== "__all__" && l.service !== serviceFilter) return false;
|
||||||
if (q && !l.line.toLowerCase().includes(q)) return false;
|
if (q && !l.msg.toLowerCase().includes(q)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [lines, serviceFilter, search]);
|
}, [lines, serviceFilter, search]);
|
||||||
@@ -124,7 +162,7 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num
|
|||||||
}, [filtered, autoScroll]);
|
}, [filtered, autoScroll]);
|
||||||
|
|
||||||
const download = () => {
|
const download = () => {
|
||||||
const blob = new Blob([filtered.map((l) => l.line).join("\n")], {
|
const blob = new Blob([filtered.map((l) => l.raw).join("\n")], {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
});
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
@@ -212,10 +250,13 @@ export function LogViewer({ stackId, agentId }: { stackId: string; agentId?: num
|
|||||||
key={i}
|
key={i}
|
||||||
className={`whitespace-pre-wrap break-all py-0.5 pl-2 ${style.row}`}
|
className={`whitespace-pre-wrap break-all py-0.5 pl-2 ${style.row}`}
|
||||||
>
|
>
|
||||||
|
{l.time && (
|
||||||
|
<span className="mr-2 text-slate-500">{l.time}</span>
|
||||||
|
)}
|
||||||
{l.service && (
|
{l.service && (
|
||||||
<span className={`mr-2 ${colorFor(l.service)}`}>{l.service}</span>
|
<span className={`mr-2 ${colorFor(l.service)}`}>{l.service}</span>
|
||||||
)}
|
)}
|
||||||
<span className={style.text}>{l.line}</span>
|
<span className={style.text}>{l.msg}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user