Improve desktop drawer detail layout

This commit is contained in:
rcourtman
2026-08-26 10:11:08 +01:00
parent a8e84b4011
commit b727bc62e4
5 changed files with 126 additions and 72 deletions
@@ -3716,7 +3716,15 @@ compact `DetailSectionTable` rows as Overview; those facts must not switch to a
provider-local card mosaic or require another tap. Only genuinely large or
interactive provider-support content belongs behind
`TechnicalDetailsDisclosure`, which owns its collapsed, lazily mounted
boundary. Monitoring mode, lifecycle, notes, maintenance,
boundary. `DetailSectionTable` keeps the single bordered table at narrow widths
where density matters, then presents those same canonical rows as bounded
section cards on desktop. Desktop cards share the available row width, stretch
to the same row height, balance five- and six-section drawers across three-card
rows, and use a bounded local label column with left-aligned values so the
layout has no ragged fixed-width island, stranded full-width final card, or
full-drawer scan distance. The responsive presentation stays owned by the shared primitive;
provider drawers must not fork their own desktop card renderers. Monitoring
mode, lifecycle, notes, maintenance,
automatic-action policy, saved access configuration, and action audit belong
to a dedicated `Manage` tab and must not render inside Overview. Guest, node,
Docker-host, and unified-resource drawers must compose these shared primitives
+20 -27
View File
@@ -1,44 +1,37 @@
{
"version": 1,
"base_sha": "3b866458f665ff7be80e611cc60286d622a66e92",
"verified_at": "2026-08-26T09:20:54Z",
"base_sha": "a8e84b4011afec482def5775b776b0f24a30d412",
"verified_at": "2026-08-26T09:27:37Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
"frontend-modern/src/components/Workloads/useGuestRowState.ts",
"frontend-modern/src/components/Workloads/workloadSelectors.ts",
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts"
],
"changed_paths": ["frontend-modern/src/components/shared/DetailSectionTable.tsx"],
"content_sha256": {
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "e5bdf0f4bcb070227c7c40d512863eaff1bcb80db53c19d59a9b9085b648d344",
"frontend-modern/src/components/Workloads/useGuestRowState.ts": "591c98dbdb3213636ae5465f65fff4df03882ef428b57589c33eec36dc22fb85",
"frontend-modern/src/components/Workloads/workloadSelectors.ts": "98fe0f042d5e114f5b3b99080cc894e7da99d490f208ce6cd94867aa5a8aaff3",
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts": "085dbf2b61d3cd87aec04b54aa3fcc15f0a12d3d70c4014830c45a807788cb79"
"frontend-modern/src/components/shared/DetailSectionTable.tsx": "07a1cd5c6489fe055842417468014bdfee01aa79785b531f15cea69c0532796a"
},
"routes": [
"/proxmox/overview"
],
"routes": ["/proxmox/overview", "/docker/overview"],
"viewports": [
{
"width": 1920,
"height": 1200
"height": 600
},
{
"width": 768,
"height": 1000
"width": 1280,
"height": 800
},
{
"width": 390,
"height": 844
}
],
"states": [
"loaded grouped guest table with running and stopped VM and LXC rows",
"wide table with the shared Info/ID column visible",
"narrow table with the responsive Info/ID column visible",
"active ascending and descending ID sort indicators"
"Proxmox guest drawer with four detail sections filling one equal-height desktop row",
"Docker host drawer with five detail sections balanced across three- and two-card desktop rows",
"phone detail sections retained the compact single-table presentation"
],
"interactions": [
"loaded the Proxmox overview from the final source at desktop and narrow widths",
"clicked the shared ID header to sort ascending and confirmed the first group ordered IDs from 663 upward",
"clicked the shared ID header again to sort descending and confirmed the first group ordered IDs from 680 downward",
"confirmed the ID sort indicator and centered header-cell alignment remain coherent at 1920px and 768px",
"confirmed the responsive table remained readable without clipping the changed header and restored the viewport override"
"opened the backup-orchestrator-311 Proxmox guest drawer at 1920 by 600 and measured four 432-pixel cards filling the 1752-pixel detail row with equal 112-pixel heights",
"rechecked the Proxmox guest drawer at 1280 by 800 and confirmed four 273-pixel cards, a 124-pixel label-to-value distance, no truncation of Node agent connected, and no horizontal overflow",
"opened the auth-service-01 Docker host drawer at 1280 by 800 and confirmed five sections balance into three 367-pixel cards and two 555-pixel cards without a stranded full-width final card",
"measured the shared detail renderer at 390 by 844 and confirmed native table, table-row-group, and table-row display with no horizontal overflow",
"restored the browser viewport override to 1280 by 800"
]
}
@@ -24,54 +24,64 @@ const detailValueToneClass = (tone: DetailValueTone | undefined): string => {
return 'text-base-content';
};
const detailSectionDesktopBasisClass = (sectionCount: number): string =>
sectionCount === 5 || sectionCount === 6
? 'lg:basis-[calc(33.333%-0.5rem)]'
: 'lg:basis-[calc(25%-0.5rem)]';
export const DetailSectionTable: Component<{
sections: DetailSection[];
class?: string;
}> = (props) => (
<div class={props.class ?? 'overflow-hidden rounded border border-border bg-surface'}>
<Table class="w-full table-fixed text-[11px]">
<TableBody class="divide-y divide-border">
<For each={props.sections}>
{(section) => (
<>
<TableRow class="bg-surface-alt">
<TableHead
colspan={2}
class="px-2 py-1 text-left text-[10px] font-semibold uppercase tracking-wide text-muted"
>
{section.label}
</TableHead>
</TableRow>
<For each={section.rows}>
{(row) => (
<TableRow>
<TableCell class="w-[38%] px-2 py-1 align-top text-muted">
{row.label}
</TableCell>
<TableCell
class={`px-2 py-1 text-right align-top font-medium ${detailValueToneClass(
row.tone,
)}`}
<div
class={`${props.class ?? 'overflow-hidden rounded border border-border bg-surface'} lg:overflow-visible lg:border-0 lg:bg-transparent`}
>
<Table
class="w-full table-fixed text-[11px] lg:flex lg:flex-wrap lg:items-stretch lg:gap-2"
wrapperClass="lg:overflow-visible"
>
<For each={props.sections}>
{(section) => (
<TableBody
class={`divide-y divide-border lg:flex lg:min-w-[16rem] lg:flex-1 lg:flex-col lg:overflow-hidden lg:rounded lg:border lg:border-border lg:bg-surface lg:p-3 lg:shadow-sm lg:divide-y-0 ${detailSectionDesktopBasisClass(props.sections.length)}`}
>
<TableRow class="bg-surface-alt lg:mb-1 lg:block lg:bg-transparent lg:hover:bg-transparent">
<TableHead
colspan={2}
class="px-2 py-1 text-left text-[10px] font-semibold uppercase tracking-wide text-muted lg:block lg:px-0 lg:pb-1 lg:pt-0 lg:text-base-content"
>
{section.label}
</TableHead>
</TableRow>
<For each={section.rows}>
{(row) => (
<TableRow class="lg:grid lg:min-w-0 lg:grid-cols-[7rem_minmax(0,1fr)] lg:items-start lg:gap-3 lg:py-0.5 lg:hover:bg-transparent">
<TableCell class="w-[38%] px-2 py-1 align-top text-muted lg:w-auto lg:px-0 lg:py-0">
{row.label}
</TableCell>
<TableCell
class={`px-2 py-1 text-right align-top font-medium lg:min-w-0 lg:px-0 lg:py-0 lg:text-left ${detailValueToneClass(
row.tone,
)}`}
title={row.title ?? row.value}
>
<span
title={row.title ?? row.value}
class={
row.wrap
? 'block whitespace-normal break-words text-left leading-snug'
: 'block truncate'
}
>
<span
title={row.title ?? row.value}
class={
row.wrap
? 'block whitespace-normal break-words text-left leading-snug'
: 'block truncate'
}
>
{row.value}
</span>
</TableCell>
</TableRow>
)}
</For>
</>
)}
</For>
</TableBody>
{row.value}
</span>
</TableCell>
</TableRow>
)}
</For>
</TableBody>
)}
</For>
</Table>
</div>
);
@@ -7926,6 +7926,11 @@ describe('shared primitive guardrails', () => {
expect(detailSectionTableSource).toContain('DetailSectionTable');
expect(detailSectionTableSource).toContain('InlineDetailPanel');
expect(detailSectionTableSource).toContain('ObjectDrawerHeader');
expect(detailSectionTableSource).toContain('lg:basis-[calc(25%-0.5rem)]');
expect(detailSectionTableSource).toContain('lg:basis-[calc(33.333%-0.5rem)]');
expect(detailSectionTableSource).toContain('lg:grid-cols-[7rem_minmax(0,1fr)]');
expect(detailSectionTableSource).toContain('lg:text-left');
expect(detailSectionTableSource).toContain('lg:divide-y-0');
for (const source of [
resourceDetailDrawerKubernetesModelSource,
@@ -57,8 +57,8 @@ describe('DetailSectionTable', () => {
expect(formatDetailCountValue(undefined, 'disk')).toBeNull();
});
it('renders section tables with shared value tone classes', () => {
render(() => (
it('keeps compact table rows on narrow screens and bounded section cards on desktop', () => {
const { container } = render(() => (
<DetailSectionTable
sections={[
{
@@ -68,6 +68,10 @@ describe('DetailSectionTable', () => {
{ label: 'Resource', value: 'tower', title: 'tower.example.test' },
],
},
{
label: 'Runtime',
rows: [{ label: 'Kernel', value: '6.8.0' }],
},
]}
/>
));
@@ -76,6 +80,40 @@ describe('DetailSectionTable', () => {
expect(screen.getByText('Severity')).toBeInTheDocument();
expect(screen.getByText('Warning').closest('td')).toHaveClass('text-amber-700');
expect(screen.getByText('tower').closest('td')).toHaveAttribute('title', 'tower.example.test');
const table = container.querySelector('table');
expect(table).toHaveClass('table-fixed', 'lg:flex', 'lg:flex-wrap', 'lg:items-stretch');
const sections = container.querySelectorAll('tbody');
expect(sections).toHaveLength(2);
expect(sections[0]).toHaveClass(
'lg:flex',
'lg:flex-1',
'lg:basis-[calc(25%-0.5rem)]',
'lg:rounded',
'lg:border',
'lg:p-3',
);
expect(screen.getByText('Severity').closest('tr')).toHaveClass(
'lg:grid',
'lg:grid-cols-[7rem_minmax(0,1fr)]',
'lg:gap-3',
);
expect(screen.getByText('Warning').closest('td')).toHaveClass('lg:text-left');
});
it('balances five desktop sections across three- and two-card rows', () => {
const { container } = render(() => (
<DetailSectionTable
sections={Array.from({ length: 5 }, (_, index) => ({
label: `Section ${index + 1}`,
rows: [{ label: 'Value', value: String(index + 1) }],
}))}
/>
));
const sections = container.querySelectorAll('tbody');
expect(sections).toHaveLength(5);
sections.forEach((section) => expect(section).toHaveClass('lg:basis-[calc(33.333%-0.5rem)]'));
});
it('lazily renders technical details with the same compact section rows', () => {