Split dashboard metric bar runtime owners

This commit is contained in:
rcourtman 2026-03-21 21:27:22 +00:00
parent 5117ed0abc
commit ce9974d1cf
9 changed files with 266 additions and 87 deletions

View file

@ -39,33 +39,38 @@ regression protection.
17. `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
18. `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts`
19. `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`
20. `frontend-modern/src/components/Dashboard/DiskList.tsx`
21. `frontend-modern/src/components/Dashboard/diskListModel.ts`
22. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
23. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
24. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
25. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
26. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
27. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
28. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
29. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
30. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
31. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
32. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
33. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
34. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
35. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
36. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
37. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
38. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
39. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
40. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
41. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
42. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
43. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
44. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
45. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
46. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
20. `frontend-modern/src/components/Dashboard/MetricBar.tsx`
21. `frontend-modern/src/components/Dashboard/metricBarModel.ts`
22. `frontend-modern/src/components/Dashboard/useMetricBarState.ts`
23. `frontend-modern/src/components/Dashboard/DiskList.tsx`
24. `frontend-modern/src/components/Dashboard/diskListModel.ts`
25. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
26. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
27. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
28. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
29. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
30. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
31. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
32. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
33. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
34. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
35. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
36. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
37. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
38. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
39. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
40. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx`
41. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx`
42. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
43. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
44. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
45. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
46. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
47. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
48. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
49. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
50. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
51. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
## Shared Boundaries
@ -92,6 +97,7 @@ regression protection.
13. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts` and `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
14. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
15. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
16. Extend metric-bar width, label-fit logic, and resize-observer runtime through `frontend-modern/src/components/Dashboard/metricBarModel.ts` and `frontend-modern/src/components/Dashboard/useMetricBarState.ts` rather than rebuilding metric-local state and threshold mapping inside `frontend-modern/src/components/Dashboard/MetricBar.tsx`
## Forbidden Paths
@ -183,6 +189,14 @@ resize-observer plus tooltip lifecycle live in
Future memory-bar runtime changes must extend through those owners instead of
reintroducing mixed resize state, balloon branching, and tooltip shaping into
the shell.
The dashboard metric bar now follows that same pattern: the shell stays in
`frontend-modern/src/components/Dashboard/MetricBar.tsx`, while width,
show-label, sublabel-fit, and threshold-color derivation live in
`frontend-modern/src/components/Dashboard/metricBarModel.ts` and
resize-observer lifecycle lives in
`frontend-modern/src/components/Dashboard/useMetricBarState.ts`. Future
metric-bar runtime changes must extend through those owners instead of
reintroducing mixed resize state and label-fit logic into the shell.
The unified resource table hot path is now also governed as explicit
performance-owned runtime, with shared ownership against the unified-resource

View file

@ -2526,6 +2526,8 @@
"frontend-modern/src/components/Dashboard/guestDrawerModel.ts",
"frontend-modern/src/components/Dashboard/GuestRow.tsx",
"frontend-modern/src/components/Dashboard/guestRowModel.tsx",
"frontend-modern/src/components/Dashboard/MetricBar.tsx",
"frontend-modern/src/components/Dashboard/metricBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedDiskBar.tsx",
"frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx",
@ -2537,6 +2539,7 @@
"frontend-modern/src/components/Dashboard/useDiskListState.ts",
"frontend-modern/src/components/Dashboard/useGuestDrawerState.ts",
"frontend-modern/src/components/Dashboard/useGuestRowState.ts",
"frontend-modern/src/components/Dashboard/useMetricBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts",
"frontend-modern/src/components/Dashboard/useThresholdSliderState.ts",
@ -2558,9 +2561,11 @@
"frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts",
"frontend-modern/src/components/Dashboard/MetricBar.test.tsx",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx",
"frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx",
"internal/api/router_bench_test.go",
@ -2601,6 +2606,8 @@
"frontend-modern/src/components/Dashboard/guestDrawerModel.ts",
"frontend-modern/src/components/Dashboard/GuestRow.tsx",
"frontend-modern/src/components/Dashboard/guestRowModel.tsx",
"frontend-modern/src/components/Dashboard/MetricBar.tsx",
"frontend-modern/src/components/Dashboard/metricBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedDiskBar.tsx",
"frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx",
@ -2612,6 +2619,7 @@
"frontend-modern/src/components/Dashboard/useDiskListState.ts",
"frontend-modern/src/components/Dashboard/useGuestDrawerState.ts",
"frontend-modern/src/components/Dashboard/useGuestRowState.ts",
"frontend-modern/src/components/Dashboard/useMetricBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts",
"frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts",
"frontend-modern/src/components/Dashboard/useThresholdSliderState.ts",
@ -2630,11 +2638,13 @@
"frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx",
"frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts",
"frontend-modern/src/components/Dashboard/__tests__/workloadSelectors.test.ts",
"frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx",
"frontend-modern/src/components/Dashboard/MetricBar.test.tsx",
"frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx",
"frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx",
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx"

View file

@ -1,74 +1,27 @@
import { Show, createMemo, createSignal, onMount, onCleanup } from 'solid-js';
import { Show } from 'solid-js';
import { ProgressBar } from '@/components/shared/ProgressBar';
import { estimateTextWidth } from '@/utils/format';
import { getMetricColorClass } from '@/utils/metricThresholds';
import type { MetricType } from '@/utils/metricThresholds';
interface MetricBarProps {
value: number;
label: string;
sublabel?: string;
showLabel?: boolean;
type?: 'cpu' | 'memory' | 'disk' | 'generic';
resourceId?: string;
class?: string;
}
import { type MetricBarProps } from './metricBarModel';
import { useMetricBarState } from './useMetricBarState';
export function MetricBar(props: MetricBarProps) {
const width = createMemo(() => Math.min(props.value, 100));
// Track container width
const [containerWidth, setContainerWidth] = createSignal(100);
let containerRef: HTMLDivElement | undefined;
// Set up ResizeObserver to track container width changes
onMount(() => {
if (!containerRef) return;
setContainerWidth(containerRef.offsetWidth);
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerWidth(entry.contentRect.width);
}
});
observer.observe(containerRef);
onCleanup(() => observer.disconnect());
});
// Determine if sublabel fits based on estimated text width
const showSublabel = createMemo(() => {
if (props.showLabel === false) return false;
if (!props.sublabel) return false;
const fullText = `${props.label} (${props.sublabel})`;
const estimatedWidth = estimateTextWidth(fullText);
return containerWidth() >= estimatedWidth;
});
const showLabel = createMemo(() => props.showLabel !== false && props.label.trim().length > 0);
// Get color class from centralized thresholds
const progressColorClass = createMemo(() => {
const metric = props.type || 'cpu';
// 'generic' falls back to cpu thresholds
const metricType: MetricType = metric === 'generic' ? 'cpu' : metric;
return getMetricColorClass(props.value, metricType);
});
const state = useMetricBarState(props);
const presentation = state.presentation;
return (
<div ref={containerRef} class="metric-text w-full h-4 flex items-center justify-center min-w-0">
<div
ref={state.setContainerRef}
class="metric-text w-full h-4 flex items-center justify-center min-w-0"
>
<ProgressBar
value={width()}
value={presentation().width}
class={`h-full ${props.class || ''}`}
fillClass={progressColorClass()}
fillClass={presentation().progressColorClass}
label={
<Show when={showLabel()}>
<Show when={presentation().showLabel}>
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-semibold text-base-content leading-none min-w-0 overflow-hidden">
<span class="max-w-full min-w-0 whitespace-nowrap overflow-hidden text-ellipsis px-0.5 text-center">
<span>{props.label}</span>
<Show when={showSublabel()}>
<Show when={presentation().showSublabel}>
<span class="metric-sublabel font-normal text-muted"> ({props.sublabel})</span>
</Show>
</span>

View file

@ -20,6 +20,9 @@ import stackedMemoryBarStateSource from '../useStackedMemoryBarState.ts?raw';
import diskListSource from '../DiskList.tsx?raw';
import diskListModelSource from '../diskListModel.ts?raw';
import diskListStateSource from '../useDiskListState.ts?raw';
import metricBarSource from '../MetricBar.tsx?raw';
import metricBarModelSource from '../metricBarModel.ts?raw';
import metricBarStateSource from '../useMetricBarState.ts?raw';
import guestDrawerSource from '../GuestDrawer.tsx?raw';
import guestDrawerModelSource from '../guestDrawerModel.ts?raw';
import guestRowSource from '../GuestRow.tsx?raw';
@ -542,6 +545,17 @@ describe('Dashboard performance contract', () => {
expect(stackedMemoryBarModelSource).toContain('tooltipRows');
});
it('keeps metric bar runtime and derivations in canonical owners', () => {
expect(metricBarSource).toContain('useMetricBarState');
expect(metricBarSource).not.toContain('const [containerWidth, setContainerWidth] =');
expect(metricBarSource).not.toContain('const progressColorClass = createMemo(() => {');
expect(metricBarSource).not.toContain('const showSublabel = createMemo(() => {');
expect(metricBarStateSource).toContain('new ResizeObserver');
expect(metricBarModelSource).toContain('export function buildMetricBarPresentation');
expect(metricBarModelSource).toContain('estimateTextWidth');
expect(metricBarModelSource).toContain('getMetricColorClass');
});
it('keeps guest row contract and hot-path state in canonical row owners', () => {
expect(guestRowSource).toContain('useGuestRowState');
expect(guestRowSource).not.toContain('export const GUEST_COLUMNS');

View file

@ -0,0 +1,69 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from '@solidjs/testing-library';
import { useMetricBarState } from '@/components/Dashboard/useMetricBarState';
class MockResizeObserver {
callback: ResizeObserverCallback;
disconnect = vi.fn();
observe = vi.fn();
constructor(callback: ResizeObserverCallback) {
this.callback = callback;
}
trigger(width: number) {
this.callback(
[
{
contentRect: { width } as DOMRectReadOnly,
} as ResizeObserverEntry,
],
this as unknown as ResizeObserver,
);
}
}
const originalResizeObserver = globalThis.ResizeObserver;
afterEach(() => {
cleanup();
globalThis.ResizeObserver = originalResizeObserver;
});
describe('useMetricBarState', () => {
it('centralizes metric bar derivations and resize observer cleanup', () => {
const observers: MockResizeObserver[] = [];
globalThis.ResizeObserver = class extends MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
super(callback);
observers.push(this);
}
} as unknown as typeof ResizeObserver;
let captured: ReturnType<typeof useMetricBarState> | undefined;
const Harness = () => {
captured = useMetricBarState({
value: 75,
label: 'Memory',
sublabel: '6 GB / 8 GB',
type: 'memory',
});
return <div ref={captured.setContainerRef} />;
};
const { unmount } = render(() => <Harness />);
expect(captured).toBeDefined();
expect(observers).toHaveLength(1);
expect(observers[0].observe).toHaveBeenCalled();
expect(captured!.presentation().progressColorClass).toContain('warning');
observers[0].trigger(320);
expect(captured!.presentation().showSublabel).toBe(true);
unmount();
expect(observers[0].disconnect).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,42 @@
import { estimateTextWidth } from '@/utils/format';
import { getMetricColorClass } from '@/utils/metricThresholds';
import type { MetricType } from '@/utils/metricThresholds';
export interface MetricBarProps {
value: number;
label: string;
sublabel?: string;
showLabel?: boolean;
type?: 'cpu' | 'memory' | 'disk' | 'generic';
resourceId?: string;
class?: string;
}
export interface MetricBarPresentation {
progressColorClass: string;
showLabel: boolean;
showSublabel: boolean;
width: number;
}
export function buildMetricBarPresentation(
props: MetricBarProps,
containerWidth: number,
): MetricBarPresentation {
const width = Math.min(props.value, 100);
const showLabel = props.showLabel !== false && props.label.trim().length > 0;
const showSublabel =
showLabel &&
Boolean(props.sublabel) &&
containerWidth >= estimateTextWidth(`${props.label} (${props.sublabel})`);
const metric = props.type || 'cpu';
const metricType: MetricType = metric === 'generic' ? 'cpu' : metric;
return {
progressColorClass: getMetricColorClass(props.value, metricType),
showLabel,
showSublabel,
width,
};
}

View file

@ -0,0 +1,37 @@
import { createMemo, createSignal, onCleanup, onMount } from 'solid-js';
import { buildMetricBarPresentation, type MetricBarProps } from './metricBarModel';
export function useMetricBarState(props: MetricBarProps) {
const [containerWidth, setContainerWidth] = createSignal(100);
let containerRef: HTMLDivElement | undefined;
let resizeObserver: ResizeObserver | undefined;
const presentation = createMemo(() => buildMetricBarPresentation(props, containerWidth()));
onMount(() => {
if (!containerRef) {
return;
}
setContainerWidth(containerRef.offsetWidth);
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerWidth(entry.contentRect.width);
}
});
resizeObserver.observe(containerRef);
});
onCleanup(() => {
resizeObserver?.disconnect();
});
return {
presentation,
setContainerRef: (element: HTMLDivElement) => {
containerRef = element;
},
};
}

View file

@ -112,6 +112,9 @@ import guestRowStateSource from '@/components/Dashboard/useGuestRowState.ts?raw'
import dashboardDiskListSource from '@/components/Dashboard/DiskList.tsx?raw';
import dashboardDiskListModelSource from '@/components/Dashboard/diskListModel.ts?raw';
import dashboardDiskListStateSource from '@/components/Dashboard/useDiskListState.ts?raw';
import metricBarSource from '@/components/Dashboard/MetricBar.tsx?raw';
import metricBarModelSource from '@/components/Dashboard/metricBarModel.ts?raw';
import metricBarStateSource from '@/components/Dashboard/useMetricBarState.ts?raw';
import guestDrawerSource from '@/components/Dashboard/GuestDrawer.tsx?raw';
import guestDrawerModelSource from '@/components/Dashboard/guestDrawerModel.ts?raw';
import guestDrawerStateSource from '@/components/Dashboard/useGuestDrawerState.ts?raw';
@ -548,6 +551,13 @@ describe('frontend resource type boundaries', () => {
'export function buildStackedMemoryBarPresentation',
);
expect(stackedMemoryBarModelSource).toContain('const MEMORY_COLORS');
expect(metricBarSource).toContain('useMetricBarState');
expect(metricBarSource).not.toContain('const [containerWidth, setContainerWidth] =');
expect(metricBarSource).not.toContain('const progressColorClass = createMemo(() => {');
expect(metricBarSource).not.toContain('const showSublabel = createMemo(() => {');
expect(metricBarStateSource).toContain('new ResizeObserver');
expect(metricBarModelSource).toContain('export function buildMetricBarPresentation');
expect(metricBarModelSource).toContain('estimateTextWidth');
expect(workloadsSummarySource).toContain('normalizeOrgScope(getOrgID())');
expect(workloadsSummarySource).not.toContain("const DEFAULT_ORG_SCOPE = 'default'");
expect(workloadsSummarySource).not.toContain('const normalizeOrgScope =');

View file

@ -1981,6 +1981,36 @@ class SubsystemLookupTest(unittest.TestCase):
"dashboard-workload-hot-path",
)
def test_lookup_paths_assigns_dashboard_metric_bar_runtime_to_performance_and_scalability(self) -> None:
result = lookup_paths(
[
"frontend-modern/src/components/Dashboard/MetricBar.tsx",
"frontend-modern/src/components/Dashboard/metricBarModel.ts",
"frontend-modern/src/components/Dashboard/useMetricBarState.ts",
]
)
self.assertEqual(result["unowned_runtime_files"], [])
self.assertEqual(
{item["subsystem"] for item in result["impacted_subsystems"]},
{"performance-and-scalability"},
)
for file_entry in result["files"]:
self.assertEqual(file_entry["classification"], "runtime")
self.assertEqual(
{match["subsystem"] for match in file_entry["matches"]},
{"performance-and-scalability"},
)
match = file_entry["matches"][0]
self.assertEqual(
match["contract"],
"docs/release-control/v6/internal/subsystems/performance-and-scalability.md",
)
self.assertEqual(match["lane_context"]["lane_id"], "L10")
self.assertEqual(
match["verification_requirement"]["id"],
"dashboard-workload-hot-path",
)
def test_lookup_paths_assigns_settings_page_shell_to_frontend_primitives(self) -> None:
result = lookup_paths(["frontend-modern/src/components/Settings/SettingsPageShell.tsx"])
self.assertEqual(result["unowned_runtime_files"], [])