mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Improve desktop drawer detail layout
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user