From 4f30db3abc9b5cbfb6df8a107750eb48f14e4485 Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 8 Aug 2026 17:39:20 -0400 Subject: [PATCH] fix: truncate long error text in fleet prune estimate per-node rows (#1802) Unreachable nodes rendered their error string in a shrink-0 span with no width bound, so a verbose daemon or proxy error pushed the row past the card edge. Bound the error span to 55% and ellipsize, matching the existing pattern in the label-and-stop card; the full message stays available via the row title tooltip. --- .../cards/FleetPruneCard.test.tsx | 25 +++++++++++++++++++ .../FleetActions/cards/FleetPruneCard.tsx | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.test.tsx b/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.test.tsx index 6862c0f9..e7fa059a 100644 --- a/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.test.tsx +++ b/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.test.tsx @@ -126,6 +126,31 @@ it('shows 0 reclaimable ยท partial when successful targets report zero bytes', a expect(screen.getByText('PART')).toBeInTheDocument(); }); +it('truncates a long unreachable error without clipping reachable byte counts', async () => { + const longError = `Pilot tunnel is disconnected: ${'x'.repeat(400)}`; + const twoNodes = [...nodes, { id: 2, name: 'edge', status: 'offline' }] as unknown as FleetNode[]; + mockedFetch.mockImplementation((url: string) => { + if (url === '/fleet/prune/estimate') { + return Promise.resolve(jsonResponse(200, { + totalBytes: 4096, + perNode: [ + { nodeId: 1, nodeName: 'central', reclaimableBytes: 4096, reachable: true }, + { nodeId: 2, nodeName: 'edge', reclaimableBytes: 0, reachable: false, error: longError }, + ], + })); + } + return Promise.resolve(jsonResponse(404, {})); + }); + + render(); + const errorSpan = await screen.findByText(longError); + expect(errorSpan.className).toContain('truncate'); + expect(errorSpan.className).toContain('max-w-[55%]'); + expect(screen.getByTitle(longError)).toBeInTheDocument(); + const byteSpan = screen.getByText('4 KB'); + expect(byteSpan.className).not.toContain('truncate'); +}); + it('drops the unlock footer once dry run review is valid', async () => { const user = userEvent.setup(); mockedFetch.mockImplementation((url: string) => { diff --git a/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.tsx b/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.tsx index 7434eb61..198a6037 100644 --- a/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.tsx +++ b/frontend/src/components/fleet/FleetActions/cards/FleetPruneCard.tsx @@ -416,7 +416,7 @@ function EstimateSection({ estimate }: { estimate: EstimateState }) { {node.reachable ? (node.partial ? 'PART' : 'OK') : '--'} {node.nodeName} - + {node.reachable ? formatBytes(node.reclaimableBytes) : (node.error ?? 'unreachable')}