Fix infrastructure table density at tablet widths

This commit is contained in:
rcourtman 2026-04-23 22:52:22 +01:00
parent 3f21acd0da
commit f4d0006a5e
8 changed files with 465 additions and 263 deletions

View file

@ -33,6 +33,7 @@ import { ResourceDetailDrawer } from './ResourceDetailDrawer';
import { buildWorkloadsHref } from './workloadsLink';
import { ClusterDeployBanner } from './ClusterDeployBanner';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
import { resolveSummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
import {
@ -74,28 +75,28 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
width={table.resourceColumn().width}
onClick={() => table.handleSort('name')}
>
Resource {table.renderSortIndicator('name')}
{table.headerLabels().resource} {table.renderSortIndicator('name')}
</TableHead>
<TableHead
class={table.metricColumn().className}
width={table.metricColumn().width}
onClick={() => table.handleSort('cpu')}
>
CPU {table.renderSortIndicator('cpu')}
{table.headerLabels().cpu} {table.renderSortIndicator('cpu')}
</TableHead>
<TableHead
class={table.metricColumn().className}
width={table.metricColumn().width}
onClick={() => table.handleSort('memory')}
>
{table.isMobile() ? 'Mem' : 'Memory'} {table.renderSortIndicator('memory')}
{table.headerLabels().memory} {table.renderSortIndicator('memory')}
</TableHead>
<TableHead
class={table.metricColumn().className}
width={table.metricColumn().width}
onClick={() => table.handleSort('disk')}
>
Disk {table.renderSortIndicator('disk')}
{table.headerLabels().disk} {table.renderSortIndicator('disk')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
@ -103,7 +104,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
width={table.ioColumn().width}
onClick={() => table.handleSort('network')}
>
Net I/O {table.renderSortIndicator('network')}
{table.headerLabels().network} {table.renderSortIndicator('network')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
@ -111,7 +112,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
width={table.ioColumn().width}
onClick={() => table.handleSort('diskio')}
>
Disk I/O {table.renderSortIndicator('diskio')}
{table.headerLabels().diskIo} {table.renderSortIndicator('diskio')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
@ -119,7 +120,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
width={table.sourceColumn().width}
onClick={() => table.handleSort('source')}
>
Source {table.renderSortIndicator('source')}
{table.headerLabels().source} {table.renderSortIndicator('source')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
@ -127,7 +128,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
width={table.uptimeColumn().width}
onClick={() => table.handleSort('uptime')}
>
Uptime {table.renderSortIndicator('uptime')}
{table.headerLabels().uptime} {table.renderSortIndicator('uptime')}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
@ -135,7 +136,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
width={table.tempColumn().width}
onClick={() => table.handleSort('temp')}
>
Temp {table.renderSortIndicator('temp')}
{table.headerLabels().temp} {table.renderSortIndicator('temp')}
</TableHead>
</TableRow>
</TableHeader>
@ -166,7 +167,9 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
const handleGroupFocusToggle = () => {
const nextScope = groupSummaryScope();
tableProps.onGroupFocusChange?.(
nextScope && tableProps.focusedSummaryGroupId === nextScope.id ? null : nextScope?.id ?? null,
nextScope && tableProps.focusedSummaryGroupId === nextScope.id
? null
: (nextScope?.id ?? null),
);
};
const groupRowInteraction = createSummaryInteractiveRowPreviewHandlers({
@ -301,7 +304,6 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
const unifiedSourceBadges = createMemo(() =>
getUnifiedSourceBadges(table.getUnifiedSources(resource)),
);
const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0);
const policyBadges = createMemo(() =>
getResourcePolicyTableBadges(resource.policy),
);
@ -497,10 +499,16 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
</div>
}
>
<div class="grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums">
<div
class={
table.layoutMode() === 'wide'
? 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums'
: 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[10px] leading-tight tabular-nums'
}
>
<span class="inline-flex w-3 justify-center text-emerald-500"></span>
<span
class={`min-w-0 whitespace-nowrap ${networkEmphasis().className}`}
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${networkEmphasis().className}`}
title={
networkEmphasis().showOutlierHint
? `${formatSpeed(resource.network!.rxBytes)} (Top outlier)`
@ -511,7 +519,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
</span>
<span class="inline-flex w-3 justify-center text-orange-400"></span>
<span
class={`min-w-0 whitespace-nowrap ${networkEmphasis().className}`}
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${networkEmphasis().className}`}
title={
networkEmphasis().showOutlierHint
? `${formatSpeed(resource.network!.txBytes)} (Top outlier)`
@ -537,12 +545,18 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
</div>
}
>
<div class="grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums">
<div
class={
table.layoutMode() === 'wide'
? 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums'
: 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[10px] leading-tight tabular-nums'
}
>
<span class="inline-flex w-3 justify-center font-mono text-blue-500">
R
</span>
<span
class={`min-w-0 whitespace-nowrap ${diskIOEmphasis().className}`}
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${diskIOEmphasis().className}`}
title={
diskIOEmphasis().showOutlierHint
? `${formatSpeed(resource.diskIO!.readRate)} (Top outlier)`
@ -555,7 +569,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
W
</span>
<span
class={`min-w-0 whitespace-nowrap ${diskIOEmphasis().className}`}
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${diskIOEmphasis().className}`}
title={
diskIOEmphasis().showOutlierHint
? `${formatSpeed(resource.diskIO!.writeRate)} (Top outlier)`
@ -571,37 +585,12 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<div class="flex items-center justify-center gap-1">
<Show
when={hasUnifiedSources()}
fallback={
<>
<Show when={platformBadge()}>
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
<Show when={sourceBadge()}>
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
</>
}
>
<For each={unifiedSourceBadges()}>
{(badge) => (
<span class={badge.classes} title={badge.title}>
{badge.label}
</span>
)}
</For>
</Show>
</div>
<UnifiedResourceSourceBadgeCell
unifiedBadges={unifiedSourceBadges()}
platformBadge={platformBadge()}
sourceBadge={sourceBadge()}
layoutMode={table.layoutMode()}
/>
</TableCell>
<TableCell

View file

@ -26,6 +26,7 @@ import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity'
import { shouldShowResourceAlternateName } from '@/utils/resourcePolicyPresentation';
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
import {
type UnifiedResourceTableProps,
type UnifiedResourceTableState,
@ -56,47 +57,47 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
class={`text-left pl-2 sm:pl-3 ${table.resourceColumn().className}`}
width={table.resourceColumn().width}
>
Resource
{table.headerLabels().resource}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
class={table.serviceCountColumn().className}
width={table.serviceCountColumn().width}
>
Datastores
{table.headerLabels().datastores}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
class={table.serviceCountColumn().className}
width={table.serviceCountColumn().width}
>
Activity
{table.headerLabels().activity}
</TableHead>
<TableHead
class={table.serviceHealthColumn().className}
width={table.serviceHealthColumn().width}
>
Health
{table.headerLabels().health}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
class={table.sourceColumn().className}
width={table.sourceColumn().width}
>
Source
{table.headerLabels().source}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
class={table.uptimeColumn().className}
width={table.uptimeColumn().width}
>
Uptime
{table.headerLabels().uptime}
</TableHead>
<TableHead
class={table.serviceActionColumn().className}
width={table.serviceActionColumn().width}
>
Action
{table.headerLabels().action}
</TableHead>
</TableRow>
</TableHeader>
@ -110,9 +111,7 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
const displayName = createMemo(() =>
getPreferredInfrastructureDisplayName(resource),
);
const serviceLink = createMemo(
() => buildServiceDetailLinks(resource)[0] ?? null,
);
const serviceLink = createMemo(() => buildServiceDetailLinks(resource)[0] ?? null);
const statusIndicator = createMemo(() =>
getAgentStatusIndicator({ status: resource.status }),
);
@ -122,7 +121,6 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
const unifiedSourceBadges = createMemo(() =>
getUnifiedSourceBadges(table.getUnifiedSources(resource)),
);
const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0);
const healthClass = createMemo(
() =>
getServiceHealthSummaryPresentation(resource.status, pbsRow()?.health)
@ -206,7 +204,9 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
>
<div class="flex justify-center">
<Show
when={pbsRow()?.datastores != null}
@ -217,7 +217,9 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<div class="flex justify-center">
<Show
when={pbsRow()?.activity}
@ -253,41 +255,22 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
<div class="flex items-center justify-center gap-1">
<Show
when={hasUnifiedSources()}
fallback={
<>
<Show when={platformBadge()}>
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
<Show when={sourceBadge()}>
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
</>
}
>
<For each={unifiedSourceBadges()}>
{(badge) => (
<span class={badge.classes} title={badge.title}>
{badge.label}
</span>
)}
</For>
</Show>
</div>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<UnifiedResourceSourceBadgeCell
unifiedBadges={unifiedSourceBadges()}
platformBadge={platformBadge()}
sourceBadge={sourceBadge()}
layoutMode={table.layoutMode()}
/>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}>
<TableCell
classList={{
hidden: table.isMobile() || !table.isVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={resource.uptime}

View file

@ -26,6 +26,7 @@ import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity'
import { shouldShowResourceAlternateName } from '@/utils/resourcePolicyPresentation';
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
import { ResourceFacetSummary } from './ResourceFacetSummary';
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
import {
type UnifiedResourceTableProps,
type UnifiedResourceTableState,
@ -56,61 +57,61 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
class={`text-left pl-2 sm:pl-3 ${table.resourceColumn().className}`}
width={table.resourceColumn().width}
>
Resource
{table.headerLabels().resource}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
class={table.serviceQueueColumn().className}
width={table.serviceQueueColumn().width}
>
Queue
{table.headerLabels().queue}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
class={table.serviceQueueColumn().className}
width={table.serviceQueueColumn().width}
>
Deferred
{table.headerLabels().deferred}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
class={table.serviceQueueColumn().className}
width={table.serviceQueueColumn().width}
>
Hold
{table.headerLabels().hold}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
class={table.serviceCountColumn().className}
width={table.serviceCountColumn().width}
>
Nodes
{table.headerLabels().nodes}
</TableHead>
<TableHead
class={table.serviceHealthColumn().className}
width={table.serviceHealthColumn().width}
>
Health
{table.headerLabels().health}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
class={table.sourceColumn().className}
width={table.sourceColumn().width}
>
Source
{table.headerLabels().source}
</TableHead>
<TableHead
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
class={table.uptimeColumn().className}
width={table.uptimeColumn().width}
>
Uptime
{table.headerLabels().uptime}
</TableHead>
<TableHead
class={table.serviceActionColumn().className}
width={table.serviceActionColumn().width}
>
Action
{table.headerLabels().action}
</TableHead>
</TableRow>
</TableHeader>
@ -124,9 +125,7 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
const displayName = createMemo(() =>
getPreferredInfrastructureDisplayName(resource),
);
const serviceLink = createMemo(
() => buildServiceDetailLinks(resource)[0] ?? null,
);
const serviceLink = createMemo(() => buildServiceDetailLinks(resource)[0] ?? null);
const statusIndicator = createMemo(() =>
getAgentStatusIndicator({ status: resource.status }),
);
@ -136,7 +135,6 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
const unifiedSourceBadges = createMemo(() =>
getUnifiedSourceBadges(table.getUnifiedSources(resource)),
);
const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0);
const healthClass = createMemo(
() =>
getServiceHealthSummaryPresentation(resource.status, pmgRow()?.health)
@ -220,18 +218,24 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
>
<div class="flex justify-center">
<Show
when={pmgRow()?.queue != null}
fallback={<span class="text-xs text-slate-400"></span>}
>
<span class={`text-xs font-medium ${queueClass()}`}>{pmgRow()!.queue}</span>
<span class={`text-xs font-medium ${queueClass()}`}>
{pmgRow()!.queue}
</span>
</Show>
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<div class="flex justify-center">
<Show
when={pmgRow()?.deferred != null}
@ -242,7 +246,11 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}>
<TableCell
classList={{
hidden: table.isMobile() || !table.isVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={pmgRow()?.hold != null}
@ -253,7 +261,9 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<div class="flex justify-center">
<Show
when={pmgRow()?.nodes != null}
@ -277,41 +287,22 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
</div>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
<div class="flex items-center justify-center gap-1">
<Show
when={hasUnifiedSources()}
fallback={
<>
<Show when={platformBadge()}>
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
<Show when={sourceBadge()}>
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
</>
}
>
<For each={unifiedSourceBadges()}>
{(badge) => (
<span class={badge.classes} title={badge.title}>
{badge.label}
</span>
)}
</For>
</Show>
</div>
<TableCell
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
>
<UnifiedResourceSourceBadgeCell
unifiedBadges={unifiedSourceBadges()}
platformBadge={platformBadge()}
sourceBadge={sourceBadge()}
layoutMode={table.layoutMode()}
/>
</TableCell>
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}>
<TableCell
classList={{
hidden: table.isMobile() || !table.isVisible('supplementary'),
}}
>
<div class="flex justify-center">
<Show
when={resource.uptime}

View file

@ -0,0 +1,57 @@
import { For, Show, createMemo } from 'solid-js';
import type { Component } from 'solid-js';
import type { ResourceBadge } from '@/utils/resourceBadgePresentation';
import type { UnifiedResourceTableLayoutMode } from './unifiedResourceTableStateModel';
interface UnifiedResourceSourceBadgeCellProps {
unifiedBadges: ResourceBadge[];
platformBadge: ResourceBadge | null;
sourceBadge: ResourceBadge | null;
layoutMode: UnifiedResourceTableLayoutMode;
}
const getVisibleSourceBadgeLimit = (layoutMode: UnifiedResourceTableLayoutMode): number =>
layoutMode === 'wide' ? 3 : 1;
export const UnifiedResourceSourceBadgeCell: Component<UnifiedResourceSourceBadgeCellProps> = (
props,
) => {
const badges = createMemo(() => {
if (props.unifiedBadges.length > 0) return props.unifiedBadges;
return [props.platformBadge, props.sourceBadge].filter((badge): badge is ResourceBadge =>
Boolean(badge),
);
});
const visibleBadges = createMemo(() =>
badges().slice(0, getVisibleSourceBadgeLimit(props.layoutMode)),
);
const hiddenBadgeCount = createMemo(() => Math.max(0, badges().length - visibleBadges().length));
const title = createMemo(() =>
badges()
.map((badge) => badge.title ?? badge.label)
.join(', '),
);
return (
<div class="flex min-w-0 max-w-full items-center justify-center gap-1 overflow-hidden">
<For each={visibleBadges()}>
{(badge) => (
<span
class={`${badge.classes} min-w-0 max-w-full overflow-hidden px-1`}
title={badge.title}
>
<span class="min-w-0 truncate">{badge.label}</span>
</span>
)}
</For>
<Show when={hiddenBadgeCount() > 0}>
<span
class="inline-flex min-w-0 max-w-full items-center overflow-hidden rounded bg-surface-alt px-1 py-0.5 text-[10px] font-medium text-muted"
title={title()}
>
+{hiddenBadgeCount()}
</span>
</Show>
</div>
);
};

View file

@ -45,10 +45,7 @@ globalThis.ResizeObserver = class ResizeObserver {
} as unknown as typeof ResizeObserver;
const emitResizeObserverWidth = (width: number) => {
for (const callback of resizeObserverCallbacks) {
callback(
[{ contentRect: { width } } as ResizeObserverEntry],
{} as ResizeObserver,
);
callback([{ contentRect: { width } } as ResizeObserverEntry], {} as ResizeObserver);
}
};
if (typeof Element.prototype.scrollIntoView !== 'function') {
@ -209,39 +206,44 @@ describe('UnifiedResourceTable performance contract', () => {
expect(getByText('Net I/O')).toBeInTheDocument();
});
emitResizeObserverWidth(900);
emitResizeObserverWidth(820);
await waitFor(() => {
expect(getByText('Net I/O').closest('th')).toHaveClass('hidden');
expect(getByText('Source').closest('th')).toHaveClass('hidden');
expect(getByText('Net').closest('th')).not.toHaveClass('hidden');
expect(getByText('Src').closest('th')).not.toHaveClass('hidden');
expect(getByText('I/O').closest('th')).toHaveClass('hidden');
expect(getByText('Up').closest('th')).toHaveClass('hidden');
});
expect(getByText('CPU').closest('th')).not.toHaveClass('hidden');
expect(getByText('Memory')).toBeInTheDocument();
expect(getByText('Mem')).toBeInTheDocument();
expect(getByText('Disk').closest('th')).not.toHaveClass('hidden');
emitResizeObserverWidth(980);
await waitFor(() => {
expect(getByText('Net I/O').closest('th')).not.toHaveClass('hidden');
expect(getByText('Disk I/O').closest('th')).not.toHaveClass('hidden');
expect(getByText('Src').closest('th')).not.toHaveClass('hidden');
expect(getByText('Up').closest('th')).not.toHaveClass('hidden');
expect(getByText('Temp').closest('th')).not.toHaveClass('hidden');
});
emitResizeObserverWidth(1200);
await waitFor(() => {
expect(getByText('Net I/O').closest('th')).not.toHaveClass('hidden');
expect(getByText('Memory')).toBeInTheDocument();
expect(getByText('Source').closest('th')).not.toHaveClass('hidden');
expect(getByText('Uptime').closest('th')).toHaveClass('hidden');
});
emitResizeObserverWidth(1400);
await waitFor(() => {
expect(getByText('Uptime').closest('th')).not.toHaveClass('hidden');
expect(getByText('Temp').closest('th')).not.toHaveClass('hidden');
});
});
it('renders the shared facet summary component in timeline-only mode for canonical resource counts', async () => {
const { getByText, queryByText } = render(() => (
<ResourceFacetSummary
counts={{
recentChanges: 3,
recentChangeKinds: {
restart: 2,
<ResourceFacetSummary
counts={{
recentChanges: 3,
recentChangeKinds: {
restart: 2,
config_update: 1,
metric_anomaly: 1,
},
@ -283,23 +285,33 @@ describe('UnifiedResourceTable performance contract', () => {
expect(unifiedResourceTableSource).toContain('UnifiedResourceHostTableCard');
expect(unifiedResourceTableSource).toContain('UnifiedResourceServiceInfrastructureCard');
expect(unifiedResourceTableSource).toContain('data-summary-clear-surface');
expect(unifiedResourceTableSource).not.toContain('const sortedPBSResources = createMemo(() =>');
expect(unifiedResourceTableSource).not.toContain(
'const sortedPBSResources = createMemo(() =>',
);
expect(unifiedResourceTableSource).not.toContain('const getOutlierEmphasis =');
expect(unifiedResourceTableSource).not.toContain('const getPBSTableRow =');
expect(unifiedResourceTableStateSource).toContain("from './unifiedResourceTableStateModel'");
expect(unifiedResourceTableStateSource).toContain('buildHostTableItems');
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableColumnPresentations');
expect(unifiedResourceTableStateSource).toContain(
'getUnifiedResourceTableColumnPresentations',
);
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableHeaderLabels');
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableLayoutMode');
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableShellClass');
expect(unifiedResourceTableStateSource).toContain('useTableWindowing');
expect(unifiedResourceTableStateSource).toContain('useUnifiedResourceTableViewportSync');
expect(unifiedResourceTableStateSource).toContain('clearPinnedSummaryScope?: () => void;');
expect(unifiedResourceTableStateSource).toContain('showHostClearAction');
expect(unifiedResourceTableStateSource).toContain('showServiceClearAction');
expect(unifiedResourceTableStateSource).not.toContain('const resourceColumnStyle = createMemo(() =>');
expect(unifiedResourceTableStateSource).not.toContain(
'const resourceColumnStyle = createMemo(() =>',
);
expect(unifiedResourceHostTableCardSource).not.toContain('style={');
expect(unifiedResourcePBSTableSectionSource).not.toContain('style={');
expect(unifiedResourcePMGTableSectionSource).not.toContain('style={');
expect(unifiedResourceTableStateSource).not.toContain("const showGroupHeaders = props.groupingMode === 'grouped'");
expect(unifiedResourceTableStateSource).not.toContain(
"const showGroupHeaders = props.groupingMode === 'grouped'",
);
expect(unifiedResourceTableStateSource).not.toContain('const items: HostTableItem[] = [];');
expect(unifiedResourceTableStateSource).not.toContain('window.addEventListener');
expect(unifiedResourceTableStateSource).not.toContain('getBoundingClientRect');
@ -314,6 +326,12 @@ describe('UnifiedResourceTable performance contract', () => {
expect(unifiedResourceTableStateModelSource).toContain(
'export const getUnifiedResourceTableColumnPresentations',
);
expect(unifiedResourceTableStateModelSource).toContain(
'export const getUnifiedResourceTableHeaderLabels',
);
expect(unifiedResourceTableStateModelSource).toContain(
'export const getUnifiedResourceTableLayoutMode',
);
expect(unifiedResourceTableStateModelSource).toContain(
'export const getUnifiedResourceTableShellClass',
);
@ -324,7 +342,7 @@ describe('UnifiedResourceTable performance contract', () => {
it('keeps PBS activity projection in the shared table-model owner', () => {
expect(unifiedResourceTableModelSource).toContain('getPbsActivitySummary');
expect(unifiedResourcePBSTableSectionSource).toContain('Activity');
expect(unifiedResourcePBSTableSectionSource).toContain('headerLabels().activity');
expect(unifiedResourcePBSTableSectionSource).not.toContain('backupJobs');
expect(unifiedResourcePBSTableSectionSource).not.toContain('syncJobs');
expect(unifiedResourcePBSTableSectionSource).not.toContain('getPbsActivitySummary');
@ -351,13 +369,13 @@ describe('UnifiedResourceTable performance contract', () => {
expect(infrastructureSummaryStateSource).not.toContain(
'const match = allSeries.find((series) => series.id === focused);',
);
expect(infrastructureSummaryStateSource).not.toContain(
"displaySeries().map((series) => ({",
);
expect(infrastructureSummaryStateSource).not.toContain('displaySeries().map((series) => ({');
expect(infrastructureSummaryStateSource).not.toContain(
"isAwaitingFirstSample() ? 'Gathering first sample…' : 'Building trend history…'",
);
expect(infrastructureSummaryStateSource).not.toContain("fetchFailed() ? 'Trend data unavailable' : emptyHistoryLabel()");
expect(infrastructureSummaryStateSource).not.toContain(
"fetchFailed() ? 'Trend data unavailable' : emptyHistoryLabel()",
);
expect(infrastructureSummaryModelSource).toContain(
'export function buildInfrastructureSummarySeries',
);
@ -380,7 +398,9 @@ describe('UnifiedResourceTable performance contract', () => {
expect(frontendIndexCssSource).toContain('--color-summary-row-bg');
expect(frontendIndexCssSource).toContain('--color-summary-row-accent');
expect(summaryInteractionA11ySource).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(summaryInteractionA11ySource).toContain('createSummaryInteractiveActionKeydownHandler');
expect(summaryInteractionA11ySource).toContain(
'createSummaryInteractiveActionKeydownHandler',
);
expect(summaryRowActionButtonSource).toContain('SummaryRowActionButton');
expect(summaryRowActionButtonSource).toContain('aria-controls');
expect(summaryRowActionButtonSource).toContain('aria-expanded');
@ -400,7 +420,9 @@ describe('UnifiedResourceTable performance contract', () => {
expect(unifiedResourceHostTableCardSource).not.toContain('kind="scope"');
expect(unifiedResourceHostTableCardSource).toContain('SummaryTableCardHeader');
expect(unifiedResourceHostTableCardSource).toContain('onClear={tableProps.clearPinnedSummaryScope}');
expect(unifiedResourceHostTableCardSource).toContain(
'onClear={tableProps.clearPinnedSummaryScope}',
);
expect(unifiedResourceServiceInfrastructureCardSource).toContain('SummaryTableCardHeader');
expect(unifiedResourceServiceInfrastructureCardSource).toContain('showClearAction');
expect(unifiedResourceServiceInfrastructureCardSource).toContain(

View file

@ -9,6 +9,8 @@ import {
getHostSpacerHeights,
getNextUnifiedResourceTableSortState,
getUnifiedResourceTableColumnPresentations,
getUnifiedResourceTableHeaderLabels,
getUnifiedResourceTableLayoutMode,
getUnifiedResourceTableShellClass,
getUnifiedResourceTableSortIndicator,
getUnifiedSources,
@ -80,9 +82,11 @@ describe('unifiedResourceTableStateModel', () => {
);
expect(getVisibleHostTableItems(items, false, 1, 2)).toEqual(items);
expect(getVisibleHostTableItems(items, true, 1, 3).map((item) =>
item.type === 'row' ? item.resource.id : item.type,
)).toEqual(['b', 'c']);
expect(
getVisibleHostTableItems(items, true, 1, 3).map((item) =>
item.type === 'row' ? item.resource.id : item.type,
),
).toEqual(['b', 'c']);
expect(getHostSpacerHeights(items.length, 1, 3, true, 40)).toEqual({
top: 40,
bottom: 0,
@ -108,27 +112,37 @@ describe('unifiedResourceTableStateModel', () => {
});
it('derives responsive column presentations and host-table visibility as pure layout policy', () => {
const mobileColumns = getUnifiedResourceTableColumnPresentations(true);
const desktopColumns = getUnifiedResourceTableColumnPresentations(false);
const mobileColumns = getUnifiedResourceTableColumnPresentations('mobile');
const tabletColumns = getUnifiedResourceTableColumnPresentations('tablet');
const compactColumns = getUnifiedResourceTableColumnPresentations('compact');
const wideColumns = getUnifiedResourceTableColumnPresentations('wide');
expect(getUnifiedResourceTableShellClass(true)).toContain('table-fixed');
expect(getUnifiedResourceTableShellClass(true)).toContain('min-w-full');
expect(getUnifiedResourceTableShellClass(false)).toContain('min-w-[max-content]');
// Mobile uses percentage widths so the visible-column set fills the
// viewport without horizontal overflow. Host rows render
// Resource + CPU + Memory + Disk = 40 + 3×20 = 100%; service (PBS/PMG)
// rows render Resource + Health + Action = 40 + 36 + 24 = 100%. Columns
// hidden at mobile keep placeholder percentages that never paint.
expect(getUnifiedResourceTableShellClass('mobile')).toContain('table-fixed');
expect(getUnifiedResourceTableShellClass('mobile')).toContain('min-w-full');
expect(getUnifiedResourceTableShellClass('compact')).toContain('min-w-full');
expect(getUnifiedResourceTableShellClass('wide')).toContain('min-w-full');
expect(getUnifiedResourceTableShellClass('wide')).not.toContain('min-w-[max-content]');
// Mobile and tablet use percentage widths so the visible-column set fills
// the table surface without horizontal overflow. Wider modes keep all
// host columns visible while compressing their tracks before any
// lower-priority columns are dropped.
expect(mobileColumns.resourceColumn.width).toBe('40%');
expect(mobileColumns.metricColumn.width).toBe('20%');
expect(mobileColumns.serviceHealthColumn.width).toBe('36%');
expect(mobileColumns.serviceActionColumn.width).toBe('24%');
expect(desktopColumns.resourceColumn.className).toContain('min-w-[220px]');
expect(desktopColumns.resourceColumn.className).toContain('max-w-[220px]');
expect(desktopColumns.resourceColumn.width).toBe(220);
expect(desktopColumns.metricColumn.width).toBe(144);
expect(desktopColumns.ioColumn.width).toBe(192);
expect(desktopColumns.sourceColumn.width).toBe(144);
expect(tabletColumns.resourceColumn.width).toBe('26%');
expect(tabletColumns.ioColumn.width).toBe('18%');
expect(tabletColumns.sourceColumn.width).toBe('17%');
expect(compactColumns.resourceColumn.width).toBe('18%');
expect(compactColumns.metricColumn.width).toBe('10.5%');
expect(compactColumns.tempColumn.width).toBe('7%');
expect(wideColumns.resourceColumn.width).toBe('18%');
expect(wideColumns.ioColumn.width).toBe('12.5%');
expect(wideColumns.serviceActionColumn.width).toBe('16%');
expect(getUnifiedResourceTableHeaderLabels('wide').memory).toBe('Memory');
expect(getUnifiedResourceTableHeaderLabels('compact').memory).toBe('Mem');
expect(getUnifiedResourceTableHeaderLabels('tablet').network).toBe('Net');
expect(getUnifiedResourceTableHeaderLabels('mobile').datastores).toBe('Stores');
expect(shouldShowUnifiedResourceHostTable(0, 0)).toBe(true);
expect(shouldShowUnifiedResourceHostTable(0, 2)).toBe(false);
expect(shouldShowUnifiedResourceHostTable(3, 2)).toBe(true);
@ -137,20 +151,27 @@ describe('unifiedResourceTableStateModel', () => {
it('derives infrastructure table breakpoints from the measured table surface width', () => {
expect(normalizeUnifiedResourceTableLayoutWidth(820.4)).toBe(820);
expect(normalizeUnifiedResourceTableLayoutWidth(null, 700)).toBe(700);
expect(shouldUseUnifiedResourceTableMobileLayout(767)).toBe(true);
expect(shouldUseUnifiedResourceTableMobileLayout(768)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('primary', 700)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('secondary', 1119)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('secondary', 1120)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('supplementary', 1359)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('supplementary', 1360)).toBe(true);
expect(getUnifiedResourceTableLayoutMode(699)).toBe('mobile');
expect(getUnifiedResourceTableLayoutMode(700)).toBe('tablet');
expect(getUnifiedResourceTableLayoutMode(899)).toBe('tablet');
expect(getUnifiedResourceTableLayoutMode(900)).toBe('compact');
expect(getUnifiedResourceTableLayoutMode(1159)).toBe('compact');
expect(getUnifiedResourceTableLayoutMode(1160)).toBe('wide');
expect(shouldUseUnifiedResourceTableMobileLayout(699)).toBe(true);
expect(shouldUseUnifiedResourceTableMobileLayout(700)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('primary', 640)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('secondary', 699)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('secondary', 700)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('supplementary', 899)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('supplementary', 900)).toBe(true);
expect(isUnifiedResourceTableColumnVisible('detailed', 1159)).toBe(false);
expect(isUnifiedResourceTableColumnVisible('detailed', 1160)).toBe(true);
});
it('reads unified source tags from platform data without hook state', () => {
expect(getUnifiedSources(makeResource('a', { platformData: { sources: ['proxmox', 'agent'] } }))).toEqual([
'proxmox',
'agent',
]);
expect(
getUnifiedSources(makeResource('a', { platformData: { sources: ['proxmox', 'agent'] } })),
).toEqual(['proxmox', 'agent']);
expect(getUnifiedSources(makeResource('b', { platformData: {} }))).toEqual([]);
});
});

View file

@ -36,15 +36,19 @@ export type HostTableItem = HostTableHeaderItem | HostTableResourceItem;
export const HOST_TABLE_ESTIMATED_ROW_HEIGHT = 40;
export const HOST_TABLE_WINDOW_SIZE = 137;
export const UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH = 1024;
export const UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH = 768;
export const UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH = 700;
export const UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH = 900;
export const UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH = 1160;
export const UNIFIED_RESOURCE_TABLE_COLUMN_BREAKPOINTS: Record<ColumnPriority, number> = {
essential: 0,
primary: 640,
secondary: 1120,
supplementary: 1360,
detailed: 1536,
secondary: UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH,
supplementary: UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH,
detailed: UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH,
};
export type UnifiedResourceTableLayoutMode = 'mobile' | 'tablet' | 'compact' | 'wide';
export const normalizeUnifiedResourceTableLayoutWidth = (
width: number | null | undefined,
fallback: number = UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH,
@ -58,8 +62,19 @@ export const normalizeUnifiedResourceTableLayoutWidth = (
return UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH;
};
export const getUnifiedResourceTableLayoutMode = (
layoutWidth: number,
): UnifiedResourceTableLayoutMode => {
const width = normalizeUnifiedResourceTableLayoutWidth(layoutWidth);
if (width < UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH) return 'mobile';
if (width < UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH) return 'tablet';
if (width < UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH) return 'compact';
return 'wide';
};
export const shouldUseUnifiedResourceTableMobileLayout = (layoutWidth: number): boolean =>
normalizeUnifiedResourceTableLayoutWidth(layoutWidth) < UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH;
getUnifiedResourceTableLayoutMode(layoutWidth) === 'mobile';
export const isUnifiedResourceTableColumnVisible = (
priority: ColumnPriority,
@ -173,10 +188,7 @@ export const getUnifiedResourceTableSortIndicator = (
return activeDirection === 'asc' ? '▲' : '▼';
};
export const sortServiceResources = (
services: Resource[],
type: 'pbs' | 'pmg',
): Resource[] =>
export const sortServiceResources = (services: Resource[], type: 'pbs' | 'pmg'): Resource[] =>
sortResources(
services.filter((resource) => resource.type === type),
'default',
@ -211,6 +223,26 @@ export type UnifiedResourceTableColumnPresentations = {
serviceActionColumn: UnifiedResourceTableColumnPresentation;
};
export type UnifiedResourceTableHeaderLabels = {
resource: string;
cpu: string;
memory: string;
disk: string;
network: string;
diskIo: string;
source: string;
uptime: string;
temp: string;
datastores: string;
activity: string;
queue: string;
deferred: string;
hold: string;
nodes: string;
health: string;
action: string;
};
const buildUnifiedResourceTableColumnPresentation = (
className: string,
width?: string | number,
@ -219,46 +251,139 @@ const buildUnifiedResourceTableColumnPresentation = (
width,
});
export const getUnifiedResourceTableShellClass = (isMobile: boolean): string =>
`table-fixed ${isMobile ? 'min-w-full' : 'min-w-[max-content]'}`;
export const getUnifiedResourceTableShellClass = (
layoutMode: UnifiedResourceTableLayoutMode,
): string => `table-fixed min-w-full${layoutMode === 'wide' ? '' : ' text-[11px] sm:text-xs'}`;
export const getUnifiedResourceTableHeaderLabels = (
layoutMode: UnifiedResourceTableLayoutMode,
): UnifiedResourceTableHeaderLabels => {
if (layoutMode === 'wide') {
return {
resource: 'Resource',
cpu: 'CPU',
memory: 'Memory',
disk: 'Disk',
network: 'Net I/O',
diskIo: 'Disk I/O',
source: 'Source',
uptime: 'Uptime',
temp: 'Temp',
datastores: 'Datastores',
activity: 'Activity',
queue: 'Queue',
deferred: 'Deferred',
hold: 'Hold',
nodes: 'Nodes',
health: 'Health',
action: 'Action',
};
}
if (layoutMode === 'compact') {
return {
resource: 'Resource',
cpu: 'CPU',
memory: 'Mem',
disk: 'Disk',
network: 'Net I/O',
diskIo: 'Disk I/O',
source: 'Src',
uptime: 'Up',
temp: 'Temp',
datastores: 'Stores',
activity: 'Activity',
queue: 'Queue',
deferred: 'Def',
hold: 'Hold',
nodes: 'Nodes',
health: 'Health',
action: 'Open',
};
}
return {
resource: 'Resource',
cpu: 'CPU',
memory: 'Mem',
disk: 'Disk',
network: 'Net',
diskIo: 'I/O',
source: 'Src',
uptime: 'Up',
temp: 'C',
datastores: 'Stores',
activity: 'Jobs',
queue: 'Queue',
deferred: 'Def',
hold: 'Hold',
nodes: 'Nodes',
health: 'Health',
action: 'Open',
};
};
export const getUnifiedResourceTableColumnPresentations = (
isMobile: boolean,
): UnifiedResourceTableColumnPresentations => ({
// Mobile widths are percentages so visible columns fill the viewport without
// triggering horizontal scroll. Hidden columns retain placeholder widths that
// never render. Host rows show Resource + CPU + Memory + Disk (40/20/20/20 =
// 100%). Service (PBS/PMG) rows show Resource + Health + Action (40/36/24 =
// 100%). See UnifiedResourceHostTableCard / UnifiedResourcePBSTableSection /
// UnifiedResourcePMGTableSection for the matching visibility predicates.
resourceColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '40%')
: buildUnifiedResourceTableColumnPresentation('min-w-[220px] max-w-[220px]', 220),
metricColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '20%')
: buildUnifiedResourceTableColumnPresentation('min-w-[144px] max-w-[144px]', 144),
ioColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '20%')
: buildUnifiedResourceTableColumnPresentation('min-w-[192px] max-w-[192px]', 192),
sourceColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '20%')
: buildUnifiedResourceTableColumnPresentation('min-w-[144px] max-w-[144px]', 144),
uptimeColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '15%')
: buildUnifiedResourceTableColumnPresentation('min-w-[80px] max-w-[80px]', 80),
tempColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '15%')
: buildUnifiedResourceTableColumnPresentation('min-w-[60px] max-w-[70px]', 60),
serviceCountColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '20%')
: buildUnifiedResourceTableColumnPresentation('min-w-[110px] max-w-[130px]', 110),
serviceQueueColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '20%')
: buildUnifiedResourceTableColumnPresentation('min-w-[120px] max-w-[140px]', 120),
serviceHealthColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '36%')
: buildUnifiedResourceTableColumnPresentation('min-w-[140px] max-w-[170px]', 140),
serviceActionColumn: isMobile
? buildUnifiedResourceTableColumnPresentation('', '24%')
: buildUnifiedResourceTableColumnPresentation('min-w-[120px] max-w-[140px]', 120),
});
layoutMode: UnifiedResourceTableLayoutMode,
): UnifiedResourceTableColumnPresentations => {
if (layoutMode === 'mobile') {
// Mobile widths are percentages so visible columns fill the viewport
// without horizontal scroll. Hidden columns keep placeholder widths that
// never render.
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '40%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '15%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '15%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '36%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '24%'),
};
}
if (layoutMode === 'tablet') {
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '26%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '17%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '6%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '16%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '17%'),
};
}
if (layoutMode === 'compact') {
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '7%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
};
}
return {
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'),
ioColumn: buildUnifiedResourceTableColumnPresentation('', '12.5%'),
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'),
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
tempColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'),
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8.5%'),
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8.5%'),
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '16%'),
};
};

View file

@ -20,6 +20,8 @@ import {
getHostSpacerHeights,
getNextUnifiedResourceTableSortState,
getUnifiedResourceTableColumnPresentations,
getUnifiedResourceTableHeaderLabels,
getUnifiedResourceTableLayoutMode,
isUnifiedResourceTableColumnVisible,
getUnifiedResourceTableShellClass,
getUnifiedResourceTableSortIndicator,
@ -101,6 +103,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
const tableLayoutWidth = createMemo(() =>
normalizeUnifiedResourceTableLayoutWidth(tableContainerWidth(), fallbackViewportWidth()),
);
const layoutMode = createMemo(() => getUnifiedResourceTableLayoutMode(tableLayoutWidth()));
const isMobile = createMemo(() => shouldUseUnifiedResourceTableMobileLayout(tableLayoutWidth()));
const isVisible = (priority: ColumnPriority) =>
isUnifiedResourceTableColumnVisible(priority, tableLayoutWidth());
@ -108,8 +111,12 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
const split = createMemo(() => splitPrimaryAndServiceResources(props.resources));
const primaryResources = createMemo(() => split().primaryResources);
const serviceResources = createMemo(() => split().services);
const primaryResourceIds = createMemo(() => new Set(primaryResources().map((resource) => resource.id)));
const serviceResourceIds = createMemo(() => new Set(serviceResources().map((resource) => resource.id)));
const primaryResourceIds = createMemo(
() => new Set(primaryResources().map((resource) => resource.id)),
);
const serviceResourceIds = createMemo(
() => new Set(serviceResources().map((resource) => resource.id)),
);
const sortedResources = createMemo(() =>
sortResources(primaryResources(), sortKey(), sortDirection()),
@ -118,7 +125,9 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
const resolveResourceLabel = (resourceId: string): string | undefined =>
resourceLabelById().get(resourceId);
const groupedResources = createMemo(() => groupResources(sortedResources(), props.groupingMode ?? 'grouped'));
const groupedResources = createMemo(() =>
groupResources(sortedResources(), props.groupingMode ?? 'grouped'),
);
const hostTableItems = createMemo<HostTableItem[]>(() =>
buildHostTableItems(groupedResources(), props.groupingMode),
);
@ -179,15 +188,18 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
props.onExpandedResourceChange(props.expandedResourceId === resourceId ? null : resourceId);
};
const tableShellClass = createMemo(() => getUnifiedResourceTableShellClass(isMobile()));
const columnPresentations = createMemo(() => getUnifiedResourceTableColumnPresentations(isMobile()));
const tableShellClass = createMemo(() => getUnifiedResourceTableShellClass(layoutMode()));
const columnPresentations = createMemo(() =>
getUnifiedResourceTableColumnPresentations(layoutMode()),
);
const headerLabels = createMemo(() => getUnifiedResourceTableHeaderLabels(layoutMode()));
const showHostTable = createMemo(() =>
shouldShowUnifiedResourceHostTable(primaryResources().length, serviceResources().length),
);
const showHostClearAction = createMemo(() =>
Boolean(
props.focusedSummaryGroupId ||
(props.expandedResourceId && primaryResourceIds().has(props.expandedResourceId)),
(props.expandedResourceId && primaryResourceIds().has(props.expandedResourceId)),
),
);
const showServiceClearAction = createMemo(() =>
@ -195,6 +207,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
);
return {
layoutMode,
isMobile,
isVisible,
handleSort,
@ -210,6 +223,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
ioScale,
...viewportSync,
tableShellClass,
headerLabels,
resourceColumn: () => columnPresentations().resourceColumn,
metricColumn: () => columnPresentations().metricColumn,
ioColumn: () => columnPresentations().ioColumn,