fix(canvas): fit the server header and skip placeholder cards in the status summary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-21 18:10:47 +02:00
co-authored by Claude Opus 5.5
parent c4bc9ae8bf
commit 44c6322259
3 changed files with 15 additions and 5 deletions
+1 -1
View File
@@ -59,7 +59,7 @@
{#if zone.ip}
<button
type="button"
class="truncate font-mono text-[11px] text-neutral-400 hover:text-neutral-600 dark:text-fg-faint dark:hover:text-fg-dim"
class="shrink-0 font-mono text-[11px] text-neutral-400 hover:text-neutral-600 dark:text-fg-faint dark:hover:text-fg-dim"
title={showIp ? 'Hide IP address' : 'Show IP address'}
onpointerdown={e => e.stopPropagation()}
onclick={() => (showIp = !showIp)}
+4 -3
View File
@@ -12,6 +12,7 @@ export const ZONE_GAP = 72;
export const ZONE_PADDING = 24;
export const ZONE_HEADER = 40;
export const SERVER_ZONE_HEADER = 68; // server canvas: a second header row with host facts, proxy and usage
export const SERVER_ZONE_MIN_WIDTH = 560; // server canvas: room for the header (name, IP, status, host facts) above one card
export const MAX_CHILDREN = 5;
export const MAX_ROWS = 6; // auto placement wraps a type into another column after this many cards
export const NO_SERVER = 'No server';
@@ -414,7 +415,7 @@ export function zoneBoxes(graph, positions, sizes = {}) {
nodes: group.nodes.map(n => n.id),
x: minX - ZONE_PADDING,
y: minY - ZONE_PADDING - zoneHeader(group.zone),
width: maxX - minX + ZONE_PADDING * 2,
width: Math.max(maxX - minX + ZONE_PADDING * 2, group.zone?.details ? SERVER_ZONE_MIN_WIDTH : 0),
height: maxY - minY + ZONE_PADDING * 2 + zoneHeader(group.zone),
});
}
@@ -541,10 +542,10 @@ export function fitView(bounds, area, readable = false) {
return { z, x: x - bounds.minX * z, y: y - bounds.minY * z };
}
/** Resource cards per status tone ({ok, warn, bad, busy, idle}); unmanaged containers are not counted. */
/** Resource cards per status tone ({ok, warn, bad, busy, idle}); unmanaged containers and placeholder cards are not counted. */
export function statusSummary(nodes) {
const out = { ok: 0, warn: 0, bad: 0, busy: 0, idle: 0 };
for (const n of nodes || []) if (n.type !== 'container' && !isSource(n)) out[statusTone(n.status)]++;
for (const n of nodes || []) if (n.type !== 'container' && n.type !== 'empty' && !isSource(n)) out[statusTone(n.status)]++;
return out;
}
+10 -1
View File
@@ -6,6 +6,7 @@ import {
MAX_ROWS,
COLUMN_GAP,
ROW_GAP,
SERVER_ZONE_MIN_WIDTH,
ZONE_HEADER,
ZONE_PADDING,
ZONE_GAP,
@@ -130,6 +131,13 @@ test('zone boxes wrap their cards with padding and header', () => {
);
});
test('server canvas zone boxes are wide enough for their header, also with one card', () => {
const g = graph([node('a', 'application', 's1')], { zones: [{ id: 's1', name: 'One', details: {} }] });
const [box] = zoneBoxes(g, { a: { x: 0, y: 0 } }, { a: { width: 260, height: 100 } });
assert.equal(box.x, -ZONE_PADDING);
assert.equal(box.width, SERVER_ZONE_MIN_WIDTH);
});
test('zone boxes skip zones without cards', () => {
assert.deepEqual(zoneBoxes(graph([node('a', 'application', 's2')]), { a: { x: 0, y: 0 } }).map(b => b.id), ['s2']);
});
@@ -385,12 +393,13 @@ test('server view: saved groups of different environments that would overlap are
assert.ok(Math.abs(p.x - s.x) >= CARD_WIDTH || Math.abs(p.y - s.y) >= estimateNodeHeight(g.nodes[0]), 'the groups do not cover each other');
});
test('status summary counts resources by tone, without unmanaged containers', () => {
test('status summary counts resources by tone, without unmanaged containers and placeholder cards', () => {
const nodes = [
node('a', 'application', 'z', { status: 'running:healthy' }),
node('b', 'database', 'z', { status: 'exited:unhealthy' }),
node('c', 'service', 'z', { status: 'exited' }),
node('container:1', 'container', 'z', { status: 'exited' }),
node('empty:1', 'empty', 'z', { status: null }),
];
assert.deepEqual(statusSummary(nodes), { ok: 1, warn: 0, bad: 2, busy: 0, idle: 0 });
assert.equal(statusWord('exited:unhealthy'), 'Exited');