Fix platform UI consistency regressions

This commit is contained in:
rcourtman
2026-05-20 11:46:30 +01:00
parent ecd44945bc
commit 111af289ab
20 changed files with 109 additions and 56 deletions
@@ -895,7 +895,9 @@ describe('GUEST_COLUMNS', () => {
expect(nameColumn?.width).toBe('200px');
expect(nameColumn?.minWidth).toBe('180px');
expect(nameColumn?.maxWidth).toBe('220px');
expect(runtimeColumn?.width).toBe('84px');
expect(runtimeColumn?.width).toBe('104px');
expect(runtimeColumn?.minWidth).toBe('96px');
expect(runtimeColumn?.maxWidth).toBe('112px');
expect(typeColumn?.width).toBe('60px');
expect(netIoColumn?.width).toBe('170px');
expect(netIoColumn?.minWidth).toBe('170px');
@@ -985,7 +987,7 @@ describe('GUEST_COLUMNS', () => {
expect(
getGuestColumnWidthStyle('update', false, 'compact', compactDockerRuntimeColumnIds),
).toEqual({
width: '8.2569%',
width: '8.9286%',
});
});
@@ -166,9 +166,9 @@ export const GUEST_COLUMNS: ColumnDef[] = [
{
id: 'runtime',
label: 'Runtime',
width: '84px',
minWidth: '74px',
maxWidth: '96px',
width: '104px',
minWidth: '96px',
maxWidth: '112px',
toggleable: true,
kind: 'text',
},
@@ -386,7 +386,7 @@ const GUEST_COLUMN_RESPONSIVE_WEIGHTS: Record<
},
compact: {
name: 26,
runtime: 8,
runtime: 10,
type: 7,
info: 7,
vmid: 7,
@@ -398,7 +398,7 @@ const GUEST_COLUMN_RESPONSIVE_WEIGHTS: Record<
image: 18,
namespace: 11,
context: 13,
update: 9,
update: 10,
},
};
@@ -157,7 +157,7 @@ export const DockerHostsTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Hosts'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[1080px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1240px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
@@ -7,6 +7,7 @@ import { useWorkloadsState } from '@/components/Workloads/useWorkloadsState';
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
import {
PlatformErrorState,
PlatformSectionTabs,
PlatformTableEmptyState,
} from '@/features/platformPage/sharedPlatformPage';
import { DockerHostsTable } from './DockerHostsTable';
@@ -34,6 +35,7 @@ const DOCKER_WORKLOAD_STATUS_OPTIONS: readonly WorkloadsStatusOption[] = [
{ value: 'degraded', label: 'Attention' },
{ value: 'stopped', label: 'Stopped' },
];
const DOCKER_TAB_SPECS = [{ id: 'overview', label: 'Overview', path: '/docker/overview' }] as const;
const dockerIcon = () => <ContainerIcon class="h-6 w-6 text-slate-400" />;
@@ -90,6 +92,12 @@ export function DockerPageSurface() {
return (
<div data-testid="docker-page" class="space-y-3">
<PlatformSectionTabs
tabs={DOCKER_TAB_SPECS}
active="overview"
ariaLabel="Docker sections"
/>
<Show
when={!loading() || model().resources.length > 0}
fallback={
@@ -124,7 +124,7 @@ export const DockerServicesTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Swarm Services'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[900px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1080px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
@@ -30,6 +30,16 @@ vi.mock('@/components/Workloads/WorkloadsSurface', () => ({
),
}));
vi.mock('@/features/platformPage/sharedPlatformPage', async () => {
const actual = await vi.importActual<typeof import('@/features/platformPage/sharedPlatformPage')>(
'@/features/platformPage/sharedPlatformPage',
);
return {
...actual,
PlatformSectionTabs: () => <div data-testid="docker-section-tabs" />,
};
});
const makeDockerHost = (overrides: Partial<Resource> = {}): Resource => ({
id: 'agent:docker-01',
name: 'docker-01',
@@ -21,8 +21,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
@@ -144,25 +144,33 @@ export const KubernetesClustersTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Clusters'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[860px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1040px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<TableHead class={getPlatformTableHeadClass()}>Cluster</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[17%]`}>
Cluster
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}>
Context
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}>
Version
</TableHead>
<TableHead class={getPlatformTableHeadClass('right')}>Nodes</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}>
Nodes
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[8%]`}>
Pods
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[12%]`}>
Deployments
</TableHead>
<TableHead class={getPlatformTableHeadClass('right')}>CPU</TableHead>
<TableHead class={getPlatformTableHeadClass('right')}>Memory</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[15%]`}>
CPU
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[15%]`}>
Memory
</TableHead>
</TableRow>
</TableHeader>
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
@@ -194,7 +202,7 @@ export const KubernetesClustersTable: Component<{
onKeyDown={drawer.handleActivationKey(cluster)}
tabIndex={0}
>
<TableCell class={getPlatformTableCellClass()}>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -211,37 +219,37 @@ export const KubernetesClustersTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{context()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden font-mono text-[11px] text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
{version()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content tabular-nums`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{counts().nodes}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{counts().pods}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{counts().deployments}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content`}
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
>
{formatPercent(cluster.cpu?.current)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content`}
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
>
{formatPercent(cluster.memory?.current)}
</TableCell>
@@ -91,7 +91,7 @@ export const KubernetesDeploymentsTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Deployments'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[820px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1080px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
@@ -140,7 +140,7 @@ export const KubernetesNodesTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Nodes'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[1000px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1280px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
@@ -73,8 +73,18 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat
runtimeState === 'blocked' || runtimeState === 'disabled' || runtimeState === 'unavailable'
);
});
const hasLoadedPatrolEvidence = createMemo(
() =>
summaryStats().hasAnyPatrolFindings ||
(state.patrolRunHistory.value()?.length ?? 0) > 0 ||
Boolean(state.patrolStatus()),
);
const showLoadingSummary = createMemo(
() => !showRuntimeSummary() && !state.intelligenceSummary() && !state.initialSurfaceReady(),
() =>
!showRuntimeSummary() &&
!state.intelligenceSummary() &&
!state.initialSurfaceReady() &&
!hasLoadedPatrolEvidence(),
);
const assessment = createMemo(() =>
getPatrolAssessmentPresentation({
@@ -112,6 +112,18 @@ describe('PatrolIntelligenceSummary', () => {
expect(screen.queryByTestId('patrol-recommended-next-step-action')).not.toBeInTheDocument();
expect(screen.queryByTestId('patrol-summary-details-toggle')).not.toBeInTheDocument();
});
it('does not leave the loading shell behind once patrol evidence has rendered', () => {
const patrolState = {
...createPatrolState(),
initialSurfaceReady: () => false,
intelligenceSummary: () => null,
} satisfies PatrolIntelligenceState;
render(() => <PatrolIntelligenceSummary state={patrolState} />);
expect(screen.queryByTestId('patrol-summary-loading')).not.toBeInTheDocument();
});
});
function createPatrolState(): PatrolIntelligenceState {
@@ -70,7 +70,7 @@ describe('platform overview layout guardrails', () => {
expect(source).toContain('PLATFORM_HEALTH_FILTER_OPTIONS');
expect(source).not.toContain("from '@/components/shared/SearchInput'");
expect(source).not.toContain("from '@/components/shared/FilterButtonGroup'");
expect(source).not.toContain('createSignal');
expect(source).not.toContain('const [search');
}
});
@@ -97,7 +97,7 @@ describe('platform overview layout guardrails', () => {
expect(vmwarePageSurfaceSource).toContain('<VsphereHostsTable');
expect(vmwarePageSurfaceSource).toContain('<WorkloadsSurface');
expect(vmwarePageSurfaceSource).toContain('<StorageSurface');
expect(vmwarePageSurfaceSource).toContain('forcedView="pools"');
expect(vmwarePageSurfaceSource).not.toContain('forcedView="pools"');
});
it('keeps secondary overview tables from rendering duplicate standalone toolbars', () => {
@@ -113,10 +113,8 @@ describe('platform overview layout guardrails', () => {
it('keeps mobile host tables focused on useful operational columns', () => {
// Assertions use the canonical kind-based helpers
// (getPlatformTableHeadClassForKind('<kind>')) for files that have been
// migrated. KubernetesClustersTable still uses the legacy align-based
// helper because another agent has it mid-edit; the assertions below
// match that legacy form until its migration lands.
// (getPlatformTableHeadClassForKind('<kind>')) so the platform overview
// tables keep aligned metric and numeric columns across providers.
expect(dockerHostsTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Host/,
);
@@ -130,11 +128,11 @@ describe('platform overview layout guardrails', () => {
/getPlatformTableHeadClassForKind\('metric-bar'\)[\s\S]{0,200}?Disk/,
);
expect(kubernetesClustersTableSource).toContain(
'<TableHead class={getPlatformTableHeadClass()}>Cluster',
expect(kubernetesClustersTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Cluster/,
);
expect(kubernetesClustersTableSource).toContain(
"<TableHead class={getPlatformTableHeadClass('right')}>Nodes",
expect(kubernetesClustersTableSource).toMatch(
/getPlatformTableHeadClassForKind\('numeric-value'\)[\s\S]{0,200}?Nodes/,
);
expect(kubernetesNodesTableSource).toMatch(
/getPlatformTableHeadClassForKind\('name'\)[\s\S]{0,200}?Node/,
@@ -10,6 +10,7 @@ import {
import { StatusDot } from '@/components/shared/StatusDot';
import { ResponsiveMetricCell } from '@/components/shared/responsive';
import { TableCard } from '@/components/shared/TableCard';
import { TableCardHeader } from '@/components/shared/TableCardHeader';
import { NodeDrawer } from '@/components/Workloads/NodeDrawer';
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
@@ -165,6 +166,7 @@ export const ProxmoxNodesTable: Component<{
}
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title="Nodes" />
<Table class={`${getProxmoxHostTableMinWidthClass(layoutMode())} table-fixed text-xs`}>
<colgroup>
<For each={visibleColumns()}>
@@ -13,10 +13,10 @@ describe('proxmoxHostTableModel', () => {
expect(ids).toEqual(['node', 'cpu', 'memory', 'disk']);
expect(getProxmoxHostTableMinWidthClass('mobile')).toBe('min-w-full');
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '44%' });
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '23%' });
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '23%' });
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '10%' });
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '40%' });
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '20%' });
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '20%' });
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '20%' });
});
it('adds temperature and guest counts before slower-changing metadata on tablet', () => {
@@ -47,10 +47,10 @@ describe('proxmoxHostTableModel', () => {
'cts',
'cluster',
]);
expect(getProxmoxHostTableMinWidthClass('compact')).toBe('min-w-[1080px]');
expect(getProxmoxHostTableMinWidthClass('wide')).toBe('min-w-[1080px]');
expect(getProxmoxHostTableMinWidthClass('compact')).toBe('min-w-[1240px]');
expect(getProxmoxHostTableMinWidthClass('wide')).toBe('min-w-[1240px]');
expect(getProxmoxHostColumnWidthStyle('cluster', 'compact', compactIds)).toEqual({
width: '13.0435%',
width: '10.989%',
});
});
});
@@ -130,7 +130,7 @@ describe('proxmoxPageModel', () => {
]);
});
it('hides Proxmox sub-tabs when the connected estate has no matching resource evidence', () => {
it('keeps Replication available for a PVE estate without recent replication events', () => {
const model = buildProxmoxPageModel([
makeResource({
id: 'pve-node-1',
@@ -148,7 +148,10 @@ describe('proxmoxPageModel', () => {
}),
]);
expect(buildVisibleProxmoxTabSpecs(model).map((tab) => tab.id)).toEqual(['overview']);
expect(buildVisibleProxmoxTabSpecs(model).map((tab) => tab.id)).toEqual([
'overview',
'replication',
]);
});
it('resolves Proxmox suite scope from canonical platform hints', () => {
@@ -122,5 +122,5 @@ export const getProxmoxHostColumnWidthStyle = (
export const getProxmoxHostTableMinWidthClass = (
layoutMode: WorkloadTableLayoutMode,
): 'min-w-full' | 'min-w-[1080px]' =>
layoutMode === 'mobile' || layoutMode === 'tablet' ? 'min-w-full' : 'min-w-[1080px]';
): 'min-w-full' | 'min-w-[1240px]' =>
layoutMode === 'mobile' || layoutMode === 'tablet' ? 'min-w-full' : 'min-w-[1240px]';
@@ -373,11 +373,12 @@ export function buildProxmoxPageModel(resources: Resource[]): ProxmoxPageModel {
export function buildVisibleProxmoxTabSpecs(model: ProxmoxPageModel): ProxmoxTabSpec[] {
const visible = new Set<ProxmoxPageTabId>(['overview']);
const hasPveEstate = model.pveNodes.length > 0 || model.guests.length > 0;
if (model.storage.length > 0 || model.physicalDisks.length > 0) {
visible.add('storage');
}
if (model.replicationChanges.length > 0) {
if (hasPveEstate || model.replicationChanges.length > 0) {
visible.add('replication');
}
if (model.resources.some(hasBackupSignal) || model.pbs.length > 0) {
@@ -163,7 +163,7 @@ export const TrueNASSystemsTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Systems'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[1040px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1300px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
@@ -96,7 +96,6 @@ export function VmwarePageSurface() {
tableOnly
showFilterToolbar
forcedSourceFilter={VMWARE_PLATFORM_FILTER}
forcedView="pools"
filterAriaLabel="vSphere datastore filters"
filterSearchPlaceholder="Search vSphere datastores by name, host, or capacity group"
filterSearchEmptyMessage="Recent vSphere datastore searches appear here."
@@ -146,7 +146,7 @@ export const VsphereHostsTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Hosts'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[960px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[1240px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*