mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Converge platform table width calculation
This commit is contained in:
@@ -2645,6 +2645,13 @@ empty-cell markers, and tabular-number styling for dense platform table cells.
|
||||
Proxmox replication last-duration cells and Standalone availability-check poll
|
||||
interval cells must compose that primitive instead of declaring local
|
||||
seconds/minutes helpers.
|
||||
Responsive platform table width normalization is registry-backed too.
|
||||
`getPlatformTableWeightedColumnWidthStyle` owns visible-column weight
|
||||
normalization, zero-width fallback, and stable four-decimal percentage
|
||||
formatting for dense platform table models. Docker / Podman container columns
|
||||
and Proxmox host columns must keep their domain column IDs, layout breakpoints,
|
||||
and weight maps local, but must call that shared helper instead of declaring
|
||||
local `formatPercentage` / `toFixed(4)` width helpers.
|
||||
Platform table numeric fallback rendering is registry-backed too.
|
||||
`PlatformTableNumberValue` owns finite-number checking, tabular-number styling,
|
||||
custom empty-marker support, and caller-owned number formatting for dense
|
||||
|
||||
@@ -278,6 +278,12 @@ That boundary covers Proxmox replication last-sync duration cells and
|
||||
Standalone availability-check poll interval cells, and future duration-like
|
||||
platform table values must join the same shared primitive registry rule before
|
||||
adding table-local formatting.
|
||||
Responsive table layout keeps the same split: unified-resource and
|
||||
source-specific table models own which columns are visible at each breakpoint
|
||||
and the relative weights for those columns, while dense platform table
|
||||
rendering must use `getPlatformTableWeightedColumnWidthStyle` for
|
||||
visible-column percentage normalization, zero-width fallback, and stable
|
||||
percentage formatting instead of declaring local width-percent helpers.
|
||||
Optional numeric table cells follow the same split: unified-resource consumers
|
||||
own which count or replica field is meaningful, whether the domain should
|
||||
zero-default an absent scheduler/service/inventory count, whether a
|
||||
|
||||
@@ -1559,6 +1559,34 @@
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-table-weighted-column-width-style",
|
||||
"category": "platform-table-layout",
|
||||
"summary": "Responsive platform table models must use the shared weighted column-width helper for visible-column percentage normalization instead of declaring local percentage-format helpers.",
|
||||
"canonical": {
|
||||
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"export": "getPlatformTableWeightedColumnWidthStyle"
|
||||
},
|
||||
"requiredConsumers": [
|
||||
{ "path": "src/features/docker/dockerContainerTableModel.ts" },
|
||||
{ "path": "src/features/proxmox/proxmoxHostTableModel.ts" }
|
||||
],
|
||||
"forbiddenPatterns": [
|
||||
{
|
||||
"path": "src/features/docker/dockerContainerTableModel.ts",
|
||||
"patterns": ["const formatPercentage", "value.toFixed(4)"]
|
||||
},
|
||||
{
|
||||
"path": "src/features/proxmox/proxmoxHostTableModel.ts",
|
||||
"patterns": ["const formatPercentage", "value.toFixed(4)"]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-table-integer-value-format",
|
||||
"category": "platform-table-text-value",
|
||||
@@ -2793,6 +2821,33 @@
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-table-local-weighted-width-helper",
|
||||
"category": "platform-table-layout",
|
||||
"summary": "Responsive platform table models must not recreate local weighted-width percentage helpers.",
|
||||
"canonical": {
|
||||
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"export": "getPlatformTableWeightedColumnWidthStyle"
|
||||
},
|
||||
"scopes": [
|
||||
"src/features/docker/dockerContainerTableModel.ts",
|
||||
"src/features/proxmox/proxmoxHostTableModel.ts"
|
||||
],
|
||||
"extensions": [".ts"],
|
||||
"allPatterns": [
|
||||
"const formatPercentage",
|
||||
"value.toFixed(4)",
|
||||
"return { width: formatPercentage(width) }"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. Responsive platform table width normalization must use getPlatformTableWeightedColumnWidthStyle so percentage rounding and zero-width fallback stay shared.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-table-local-truenas-count-cell-class",
|
||||
"category": "platform-table-text-value",
|
||||
|
||||
@@ -149,6 +149,7 @@ import workloadsTableSource from '@/components/Workloads/WorkloadsTable.tsx?raw'
|
||||
import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw';
|
||||
import guestRowStateSource from '@/components/Workloads/useGuestRowState.ts?raw';
|
||||
import workloadSelectionStateSource from '@/components/Workloads/useWorkloadSelectionState.ts?raw';
|
||||
import dockerContainerTableModelSource from '@/features/docker/dockerContainerTableModel.ts?raw';
|
||||
import dockerContainersTableSource from '@/features/docker/DockerContainersTable.tsx?raw';
|
||||
import dockerHostsTableSource from '@/features/docker/DockerHostsTable.tsx?raw';
|
||||
import dockerNativeTableSharedSource from '@/features/docker/DockerNativeTableShared.tsx?raw';
|
||||
@@ -4323,6 +4324,82 @@ describe('shared primitive guardrails', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps responsive platform table column widths on the shared weighted helper', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
rules?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
requiredConsumers?: Array<{ path?: string }>;
|
||||
forbiddenPatterns?: Array<{ path?: string; patterns?: string[] }>;
|
||||
}>;
|
||||
patternGuards?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
allPatterns?: string[];
|
||||
scopes?: string[];
|
||||
allowedPaths?: string[];
|
||||
ignoredPaths?: string[];
|
||||
}>;
|
||||
};
|
||||
const registeredRule = registry.rules?.find(
|
||||
(rule) => rule.id === 'platform-table-weighted-column-width-style',
|
||||
);
|
||||
const localHelperGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'platform-table-local-weighted-width-helper',
|
||||
);
|
||||
const platformWidthConsumers: Array<[string, string]> = [
|
||||
['src/features/docker/dockerContainerTableModel.ts', dockerContainerTableModelSource],
|
||||
['src/features/proxmox/proxmoxHostTableModel.ts', proxmoxHostTableModelSource],
|
||||
];
|
||||
const platformWidthConsumerPaths = platformWidthConsumers.map(([path]) => path);
|
||||
|
||||
expect(registeredRule?.canonical?.path).toBe(
|
||||
'src/features/platformPage/sharedPlatformPage.tsx',
|
||||
);
|
||||
expect(registeredRule?.canonical?.export).toBe('getPlatformTableWeightedColumnWidthStyle');
|
||||
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual(
|
||||
platformWidthConsumerPaths,
|
||||
);
|
||||
expect(registeredRule?.forbiddenPatterns).toEqual([
|
||||
{
|
||||
path: 'src/features/docker/dockerContainerTableModel.ts',
|
||||
patterns: ['const formatPercentage', 'value.toFixed(4)'],
|
||||
},
|
||||
{
|
||||
path: 'src/features/proxmox/proxmoxHostTableModel.ts',
|
||||
patterns: ['const formatPercentage', 'value.toFixed(4)'],
|
||||
},
|
||||
]);
|
||||
expect(localHelperGuard?.canonical?.path).toBe(
|
||||
'src/features/platformPage/sharedPlatformPage.tsx',
|
||||
);
|
||||
expect(localHelperGuard?.canonical?.export).toBe('getPlatformTableWeightedColumnWidthStyle');
|
||||
expect(localHelperGuard?.allPatterns).toEqual([
|
||||
'const formatPercentage',
|
||||
'value.toFixed(4)',
|
||||
'return { width: formatPercentage(width) }',
|
||||
]);
|
||||
expect(localHelperGuard?.scopes).toEqual([
|
||||
'src/features/docker/dockerContainerTableModel.ts',
|
||||
'src/features/proxmox/proxmoxHostTableModel.ts',
|
||||
]);
|
||||
expect(localHelperGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(localHelperGuard?.ignoredPaths ?? []).toHaveLength(0);
|
||||
|
||||
expect(sharedPlatformPageSource).toContain(
|
||||
'export const getPlatformTableWeightedColumnWidthStyle',
|
||||
);
|
||||
|
||||
for (const [path, source] of platformWidthConsumers) {
|
||||
expect(source).toContain('getPlatformTableWeightedColumnWidthStyle');
|
||||
const forbiddenPatterns =
|
||||
registeredRule?.forbiddenPatterns?.find((entry) => entry.path === path)?.patterns ?? [];
|
||||
for (const pattern of forbiddenPatterns) {
|
||||
expect(source).not.toContain(pattern);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps platform table number-value fallbacks on the shared primitive', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
rules?: Array<{
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { JSX } from 'solid-js';
|
||||
|
||||
import type { WorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel';
|
||||
import type { PlatformTableColumnKind } from '@/features/platformPage/columnAlignment';
|
||||
import { getPlatformTableWeightedColumnWidthStyle } from '@/features/platformPage/sharedPlatformPage';
|
||||
|
||||
export type DockerContainerTableColumnId =
|
||||
| 'container'
|
||||
@@ -119,8 +120,6 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record<
|
||||
},
|
||||
};
|
||||
|
||||
const formatPercentage = (value: number): string => `${Number(value.toFixed(4))}%`;
|
||||
|
||||
export const getDockerContainerVisibleColumnsForLayout = (
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
includeRuntime: boolean,
|
||||
@@ -148,11 +147,7 @@ export const getDockerContainerColumnWidthStyle = (
|
||||
layoutMode === 'wide'
|
||||
? DOCKER_CONTAINER_DESKTOP_WIDTHS
|
||||
: DOCKER_CONTAINER_RESPONSIVE_WIDTHS[layoutMode];
|
||||
const columnWeight = weights[columnId] ?? 0;
|
||||
const totalWeight = visibleColumnIds.reduce((total, id) => total + (weights[id] ?? 0), 0);
|
||||
const width = totalWeight > 0 ? (columnWeight / totalWeight) * 100 : 0;
|
||||
|
||||
return { width: formatPercentage(width) };
|
||||
return getPlatformTableWeightedColumnWidthStyle(columnId, weights, visibleColumnIds);
|
||||
};
|
||||
|
||||
// The Docker container table already has a row detail drawer for forensic
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
filterPlatformResources,
|
||||
formatPlatformTableTextValue,
|
||||
getPlatformTableFiniteMetric,
|
||||
getPlatformTableWeightedColumnWidthStyle,
|
||||
summarizePlatformTableValues,
|
||||
type PlatformResourceStatusFilter,
|
||||
} from '../sharedPlatformPage';
|
||||
@@ -419,6 +420,33 @@ describe('PlatformTableDurationValue', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlatformTableWeightedColumnWidthStyle', () => {
|
||||
it('normalizes visible weighted table columns into stable width percentages', () => {
|
||||
const weights = {
|
||||
container: 32,
|
||||
state: 14,
|
||||
cpu: 18,
|
||||
memory: 22,
|
||||
updates: 14,
|
||||
actions: 14,
|
||||
};
|
||||
const visibleColumnIds = ['container', 'state', 'cpu', 'memory', 'updates', 'actions'] as const;
|
||||
|
||||
expect(
|
||||
getPlatformTableWeightedColumnWidthStyle('container', weights, visibleColumnIds),
|
||||
).toEqual({
|
||||
width: '28.0702%',
|
||||
});
|
||||
expect(getPlatformTableWeightedColumnWidthStyle('memory', weights, visibleColumnIds)).toEqual({
|
||||
width: '19.2982%',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a zero width when no visible weighted columns are present', () => {
|
||||
expect(getPlatformTableWeightedColumnWidthStyle('node', {}, [])).toEqual({ width: '0%' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPlatformTableIntegerValue', () => {
|
||||
it('formats rounded integer labels with canonical empty markers', () => {
|
||||
const expected = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(1235);
|
||||
|
||||
@@ -358,6 +358,21 @@ export function PlatformTableDurationValue(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const formatPlatformTableWidthPercentage = (value: number): string =>
|
||||
`${Number(value.toFixed(4))}%`;
|
||||
|
||||
export const getPlatformTableWeightedColumnWidthStyle = <ColumnId extends string>(
|
||||
columnId: ColumnId,
|
||||
weights: Partial<Record<ColumnId, number>>,
|
||||
visibleColumnIds: readonly ColumnId[],
|
||||
): JSX.CSSProperties => {
|
||||
const columnWeight = weights[columnId] ?? 0;
|
||||
const totalWeight = visibleColumnIds.reduce((total, id) => total + (weights[id] ?? 0), 0);
|
||||
const width = totalWeight > 0 ? (columnWeight / totalWeight) * 100 : 0;
|
||||
|
||||
return { width: formatPlatformTableWidthPercentage(width) };
|
||||
};
|
||||
|
||||
const platformTableIntegerFormatter = new Intl.NumberFormat(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { JSX } from 'solid-js';
|
||||
|
||||
import type { WorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel';
|
||||
import type { PlatformTableColumnKind } from '@/features/platformPage/columnAlignment';
|
||||
import { getPlatformTableWeightedColumnWidthStyle } from '@/features/platformPage/sharedPlatformPage';
|
||||
|
||||
export type ProxmoxHostTableColumnId =
|
||||
| 'node'
|
||||
@@ -82,8 +83,6 @@ const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record<
|
||||
compact: HOST_COLUMN_DESKTOP_WIDTHS,
|
||||
};
|
||||
|
||||
const formatPercentage = (value: number): string => `${Number(value.toFixed(4))}%`;
|
||||
|
||||
// Column order follows the canonical recommended ordering documented in
|
||||
// columnAlignment.ts: identity → context → bars (CPU/Memory/Disk
|
||||
// contiguous) → diagnostic (Temp) → time (Uptime) → inventory counts
|
||||
@@ -119,11 +118,7 @@ export const getProxmoxHostColumnWidthStyle = (
|
||||
): JSX.CSSProperties => {
|
||||
const weights =
|
||||
layoutMode === 'wide' ? HOST_COLUMN_DESKTOP_WIDTHS : HOST_COLUMN_RESPONSIVE_WEIGHTS[layoutMode];
|
||||
const columnWeight = weights[columnId] ?? 0;
|
||||
const totalWeight = visibleColumnIds.reduce((total, id) => total + (weights[id] ?? 0), 0);
|
||||
const width = totalWeight > 0 ? (columnWeight / totalWeight) * 100 : 0;
|
||||
|
||||
return { width: formatPercentage(width) };
|
||||
return getPlatformTableWeightedColumnWidthStyle(columnId, weights, visibleColumnIds);
|
||||
};
|
||||
|
||||
// Only the `wide` layout (>= 1440px viewport) reserves a fixed 1240px floor so
|
||||
@@ -134,5 +129,4 @@ export const getProxmoxHostColumnWidthStyle = (
|
||||
// fits the container exactly and the bars scale down gracefully instead.
|
||||
export const getProxmoxHostTableMinWidthClass = (
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
): 'min-w-full' | 'min-w-[1240px]' =>
|
||||
layoutMode === 'wide' ? 'min-w-[1240px]' : 'min-w-full';
|
||||
): 'min-w-full' | 'min-w-[1240px]' => (layoutMode === 'wide' ? 'min-w-[1240px]' : 'min-w-full');
|
||||
|
||||
Reference in New Issue
Block a user