-
+
-
+
-
+
String(Math.max(0, Math.min(value, 100)));
+ const parsePercent = (value: string) => {
+ const parsed = Number.parseFloat(value);
+ if (!Number.isFinite(parsed)) return '0';
+ return String(Math.max(0, Math.min(parsed, 100)));
+ };
return (
@@ -19,34 +25,70 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
>
{/* Stacked segments for multiple disks */}
-
+
{(segment, idx) => (
-
+ <>
+ sum + item.widthPercent, 0),
+ )}
+ y="0"
+ width={clampPercent(segment.widthPercent)}
+ height="100"
+ rx="3"
+ fill={segment.color}
+ />
+
+ sum + item.widthPercent, 0),
+ )}
+ x2={String(
+ presentation()
+ .segments.slice(0, idx() + 1)
+ .reduce((sum, item) => sum + item.widthPercent, 0),
+ )}
+ y1="0"
+ y2="100"
+ stroke="rgba(255,255,255,0.3)"
+ stroke-width="1"
+ />
+
+ >
)}
-
+
{/* Single bar for aggregate or single disk */}
-
+
+
+
{/* Label overlay */}
@@ -83,26 +125,30 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
}
>
-
+
{(disk) => (
-
+
)}
@@ -128,21 +174,33 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
classList={{ 'border-t border-border': idx() > 0 }}
>
-
- {item.label}
+
+
+
+
+ {item.label}
{item.percent} ({item.used}/{item.total})
-
)}
diff --git a/frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx b/frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx
index 5d810837c..ee538c819 100644
--- a/frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx
+++ b/frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx
@@ -15,6 +15,16 @@ global.ResizeObserver = class ResizeObserver {
unobserve = vi.fn();
};
+function getSegments(container: HTMLElement): SVGRectElement[] {
+ return Array.from(
+ container.querySelectorAll
('rect[data-stacked-memory-segment="true"]'),
+ );
+}
+
+function getSwapBar(container: HTMLElement): SVGRectElement | null {
+ return container.querySelector('rect[data-stacked-memory-swap="true"]');
+}
+
describe('StackedMemoryBar', () => {
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 200 });
@@ -65,9 +75,9 @@ describe('StackedMemoryBar', () => {
));
expect(screen.getByText('125%')).toBeInTheDocument();
- // Segment width also exceeds 100% (CSS overflow:hidden on parent clips visually)
- const segment = container.querySelector('.absolute.top-0.h-full') as HTMLElement;
- expect(segment.style.width).toBe('125%');
+ // Segment width also exceeds 100% (the SVG viewBox clips visually)
+ const segment = getSegments(container)[0];
+ expect(segment).toHaveAttribute('width', '125');
});
// ---- Segments ----
@@ -77,10 +87,10 @@ describe('StackedMemoryBar', () => {
));
// The bar fills 25%
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
expect(segments.length).toBe(1);
- expect((segments[0] as HTMLElement).style.width).toBe('25%');
- expect((segments[0] as HTMLElement).style.left).toBe('0%');
+ expect(segments[0]).toHaveAttribute('width', '25');
+ expect(segments[0]).toHaveAttribute('x', '0');
});
it('renders balloon segment when active ballooning is in effect', () => {
@@ -88,14 +98,14 @@ describe('StackedMemoryBar', () => {
const { container } = render(() => (
));
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
// Active segment + Balloon segment
expect(segments.length).toBe(2);
// Active: 2/8 = 25%
- expect((segments[0] as HTMLElement).style.width).toBe('25%');
+ expect(segments[0]).toHaveAttribute('width', '25');
// Balloon: (4/8)*100 - 25 = 25%
- expect((segments[1] as HTMLElement).style.width).toBe('25%');
- expect((segments[1] as HTMLElement).style.left).toBe('25%');
+ expect(segments[1]).toHaveAttribute('width', '25');
+ expect(segments[1]).toHaveAttribute('x', '25');
});
it('does not render balloon segment when balloon equals total', () => {
@@ -103,7 +113,7 @@ describe('StackedMemoryBar', () => {
const { container } = render(() => (
));
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
expect(segments.length).toBe(1);
});
@@ -111,7 +121,7 @@ describe('StackedMemoryBar', () => {
const { container } = render(() => (
));
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
expect(segments.length).toBe(1);
});
@@ -120,23 +130,23 @@ describe('StackedMemoryBar', () => {
const { container } = render(() => (
));
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
// Only active segment (balloon filtered out because used > balloon)
expect(segments.length).toBe(1);
});
it('renders no segments when used is 0 and total > 0', () => {
const { container } = render(() => );
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
// bytes=0 is filtered out
expect(segments.length).toBe(0);
});
it('renders percent-only segment (no bytes) when total is 0 but percentOnly > 0', () => {
const { container } = render(() => );
- const segments = container.querySelectorAll('.absolute.top-0.h-full');
+ const segments = getSegments(container);
expect(segments.length).toBe(1);
- expect((segments[0] as HTMLElement).style.width).toBe('60%');
+ expect(segments[0]).toHaveAttribute('width', '60');
});
// ---- Swap ----
@@ -151,10 +161,10 @@ describe('StackedMemoryBar', () => {
/>
));
// Swap indicator is the 3px bar at the bottom
- const swapBar = container.querySelector('.h-\\[3px\\]');
+ const swapBar = getSwapBar(container);
expect(swapBar).toBeInTheDocument();
// 1/2 = 50%
- expect((swapBar as HTMLElement).style.width).toBe('50%');
+ expect(swapBar).toHaveAttribute('width', '50');
});
it('does not render swap indicator when swapUsed is 0', () => {
@@ -166,7 +176,7 @@ describe('StackedMemoryBar', () => {
swapTotal={2 * 1024 ** 3}
/>
));
- const swapBar = container.querySelector('.h-\\[3px\\]');
+ const swapBar = getSwapBar(container);
expect(swapBar).not.toBeInTheDocument();
});
@@ -174,7 +184,7 @@ describe('StackedMemoryBar', () => {
const { container } = render(() => (
));
- const swapBar = container.querySelector('.h-\\[3px\\]');
+ const swapBar = getSwapBar(container);
expect(swapBar).not.toBeInTheDocument();
});
@@ -187,10 +197,10 @@ describe('StackedMemoryBar', () => {
swapTotal={2 * 1024 ** 3}
/>
));
- const swapBar = container.querySelector('.h-\\[3px\\]');
+ const swapBar = getSwapBar(container);
expect(swapBar).toBeInTheDocument();
// Math.min(150, 100) = 100%
- expect((swapBar as HTMLElement).style.width).toBe('100%');
+ expect(swapBar).toHaveAttribute('width', '100');
});
// ---- Sublabel (bytes display) ----
@@ -281,27 +291,27 @@ describe('StackedMemoryBar', () => {
const { container } = render(() => (
));
- const segment = container.querySelector('.absolute.top-0.h-full') as HTMLElement;
+ const segment = getSegments(container)[0];
// 25% → normal → green
- expect(segment.style.backgroundColor).toContain('34, 197, 94');
+ expect(segment.getAttribute('fill')).toContain('34, 197, 94');
});
it('uses yellow color for warning-level memory usage', () => {
const { container } = render(() => (
));
- const segment = container.querySelector('.absolute.top-0.h-full') as HTMLElement;
+ const segment = getSegments(container)[0];
// 75% → warning → yellow
- expect(segment.style.backgroundColor).toContain('234, 179, 8');
+ expect(segment.getAttribute('fill')).toContain('234, 179, 8');
});
it('uses red color for critical memory usage', () => {
const { container } = render(() => (
));
- const segment = container.querySelector('.absolute.top-0.h-full') as HTMLElement;
+ const segment = getSegments(container)[0];
// 87.5% → critical → red
- expect(segment.style.backgroundColor).toContain('239, 68, 68');
+ expect(segment.getAttribute('fill')).toContain('239, 68, 68');
});
// ---- ResizeObserver lifecycle ----
diff --git a/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx b/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx
index 182aab6ca..eb933b4f0 100644
--- a/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx
+++ b/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx
@@ -6,6 +6,9 @@ import { useStackedMemoryBarState } from './useStackedMemoryBarState';
export function StackedMemoryBar(props: StackedMemoryBarProps) {
const state = useStackedMemoryBarState(props);
const presentation = state.presentation;
+ const swapBarWidth = () => String(Math.max(0, Math.min(presentation().swapBarPercent, 100)));
+ const segmentEdge = (leftPercent: number, widthPercent: number) =>
+ String(Math.max(0, Math.min(leftPercent + widthPercent, 100)));
return (
@@ -14,29 +17,49 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
onMouseEnter={state.handleMouseEnter}
onMouseLeave={state.handleMouseLeave}
>
-
- {(segment, idx) => (
-
+
+ {(segment, idx) => (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
- )}
-
-
-
-
-
+
+
diff --git a/frontend-modern/src/components/Dashboard/WorkloadPanel.tsx b/frontend-modern/src/components/Dashboard/WorkloadPanel.tsx
index 383eb5a21..156a333c8 100644
--- a/frontend-modern/src/components/Dashboard/WorkloadPanel.tsx
+++ b/frontend-modern/src/components/Dashboard/WorkloadPanel.tsx
@@ -59,10 +59,14 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
0}>
-
+
+
+
>}>
@@ -226,10 +230,14 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
0}>
-
+
+
+
diff --git a/frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx b/frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx
index 691026daf..354d1a571 100644
--- a/frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx
+++ b/frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx
@@ -28,26 +28,14 @@ export function WorkloadTableHeader(props: WorkloadTableHeaderProps) {
return (
isSortable && props.handleSort(sortKeyForCol!)}
title={col.icon ? col.label : undefined}
>
{col.icon ?
{col.icon} : col.label}
{isSorted() && (props.sortDirection() === 'asc' ? ' ▲' : ' ▼')}
diff --git a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx
index cabaaa452..1789cacd4 100644
--- a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx
+++ b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx
@@ -836,6 +836,8 @@ describe('Dashboard performance contract', () => {
it('keeps stacked disk bar runtime and derivations in canonical owners', () => {
expect(stackedDiskBarSource).toContain('useStackedDiskBarState');
+ expect(stackedDiskBarSource).not.toContain('style={{');
+ expect(stackedDiskBarSource).not.toContain('style={');
expect(stackedDiskBarSource).not.toContain('const [containerWidth, setContainerWidth] =');
expect(stackedDiskBarSource).not.toContain('const tooltipContent = createMemo(() => {');
expect(stackedDiskBarSource).not.toContain('const SEGMENT_COLORS =');
@@ -848,6 +850,8 @@ describe('Dashboard performance contract', () => {
it('keeps stacked memory bar runtime and derivations in canonical owners', () => {
expect(stackedMemoryBarSource).toContain('useStackedMemoryBarState');
+ expect(stackedMemoryBarSource).not.toContain('style={{');
+ expect(stackedMemoryBarSource).not.toContain('style={');
expect(stackedMemoryBarSource).not.toContain('const [containerWidth, setContainerWidth] =');
expect(stackedMemoryBarSource).not.toContain('const segments = createMemo(() => {');
expect(stackedMemoryBarSource).not.toContain('const MEMORY_COLORS =');
@@ -873,6 +877,8 @@ describe('Dashboard performance contract', () => {
it('keeps enhanced CPU bar runtime and derivations in canonical owners', () => {
expect(enhancedCpuBarSource).toContain('useEnhancedCPUBarState');
+ expect(enhancedCpuBarSource).not.toContain('style={{');
+ expect(enhancedCpuBarSource).not.toContain('style={');
expect(enhancedCpuBarSource).not.toContain('const tip = useTooltip()');
expect(enhancedCpuBarSource).not.toContain('const barColor = createMemo(() =>');
expect(enhancedCpuBarSource).not.toContain('const anomalyRatio = createMemo(() =>');
@@ -887,6 +893,8 @@ describe('Dashboard performance contract', () => {
it('keeps guest row contract and hot-path state in canonical row owners', () => {
expect(guestRowSource).toContain('useGuestRowState');
expect(guestRowSource).toContain("from './GuestRowCells'");
+ expect(guestRowSource).not.toContain('style={{');
+ expect(guestRowSource).not.toContain('style={');
expect(guestRowSource).not.toContain('export const GUEST_COLUMNS');
expect(guestRowSource).not.toContain('const guestId = createMemo(');
expect(guestRowSource).not.toContain('function NetworkInfoCell(');
@@ -899,6 +907,8 @@ describe('Dashboard performance contract', () => {
expect(guestRowStateSource).toContain("from '@/routing/resourceLinks'");
expect(guestRowStateSource).toContain("from './workloadTopology'");
expect(guestRowStateSource).not.toContain("./infrastructureLink");
+ expect(guestRowStateSource).not.toContain('rowStyle');
+ expect(guestRowStateSource).not.toContain('box-shadow');
expect(guestRowStateSource).toContain('getWorkloadTypeBadge');
expect(guestRowCellsSource).toContain('export { BackupIndicator');
expect(guestRowCellsSource).toContain('function NetworkInfoCell(');
@@ -941,17 +951,23 @@ describe('Dashboard performance contract', () => {
expect(dashboardSource).not.toContain('NodeGroupHeader');
expect(dashboardWorkloadTableSource).toContain('WorkloadTableHeader');
expect(dashboardWorkloadTableSource).toContain('WorkloadPanel');
+ expect(dashboardWorkloadTableSource).not.toContain('style={{');
+ expect(dashboardWorkloadTableSource).not.toContain('style={');
expect(dashboardWorkloadTableSource).not.toContain('
getCanonicalWorkloadId(guest()))');
expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(workloadPanelSource).toContain('resolveSummaryGroupMemberInteractionState');
+ expect(workloadPanelSource).not.toContain('style={{');
+ expect(workloadPanelSource).not.toContain('style={');
expect(workloadPanelSource).not.toContain('kind="scope"');
expect(workloadPanelSource).not.toContain('leadingAction={');
expect(dashboardSelectionStateSource).toContain('activeSummaryWorkloadGroupScope');
diff --git a/frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx
index 7686ffea6..b794adba6 100644
--- a/frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx
+++ b/frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx
@@ -19,9 +19,9 @@ function makeAnomaly(overrides: Partial = {}): AnomalyReport {
};
}
-/** Select the bar fill element (absolutely-positioned div with inline width style). */
-function getBarFill(container: HTMLElement): HTMLElement | null {
- return container.querySelector('.absolute.top-0.left-0[style]');
+/** Select the bar fill element. */
+function getBarFill(container: HTMLElement): SVGRectElement | null {
+ return container.querySelector('rect[data-enhanced-cpu-fill="true"]');
}
/** Find the tooltip bar trigger element within the render container. */
@@ -77,19 +77,19 @@ describe('EnhancedCPUBar', () => {
it('sets bar width to usage percentage', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar).toHaveStyle({ width: '65%' });
+ expect(bar).toHaveAttribute('width', '65');
});
it('caps bar width at 100% when usage exceeds 100', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar).toHaveStyle({ width: '100%' });
+ expect(bar).toHaveAttribute('width', '100');
});
it('sets bar width to 0% for zero usage', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar).toHaveStyle({ width: '0%' });
+ expect(bar).toHaveAttribute('width', '0');
});
// ── Bar color classes (exact boundary tests: cpu warning=80, critical=90) ──
@@ -97,31 +97,31 @@ describe('EnhancedCPUBar', () => {
it('uses normal color class for usage below warning threshold', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar?.className).toContain('bg-metric-normal-bg');
+ expect(bar?.getAttribute('fill')).toContain('34, 197, 94');
});
it('uses warning color class at exactly the warning threshold (80)', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar?.className).toContain('bg-metric-warning-bg');
+ expect(bar?.getAttribute('fill')).toContain('234, 179, 8');
});
it('uses warning color class between warning and critical thresholds', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar?.className).toContain('bg-metric-warning-bg');
+ expect(bar?.getAttribute('fill')).toContain('234, 179, 8');
});
it('uses critical color class at exactly the critical threshold (90)', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar?.className).toContain('bg-metric-critical-bg');
+ expect(bar?.getAttribute('fill')).toContain('239, 68, 68');
});
it('uses critical color class well above critical threshold', () => {
const { container } = render(() => );
const bar = getBarFill(container);
- expect(bar?.className).toContain('bg-metric-critical-bg');
+ expect(bar?.getAttribute('fill')).toContain('239, 68, 68');
});
// ── Cores display ────────────────────────────────────────────────
diff --git a/frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx
index 764ffd354..b6da3a173 100644
--- a/frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx
+++ b/frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx
@@ -392,7 +392,7 @@ describe('GuestRow', () => {
expect(tr?.className).toContain('bg-yellow-50');
});
- it('applies box-shadow for alert accent on unacknowledged alerts', () => {
+ it('marks unacknowledged critical alerts with the canonical alert accent tone', () => {
const { container } = renderGuestRow({
guest: makeGuest(),
alertStyles: {
@@ -406,12 +406,11 @@ describe('GuestRow', () => {
},
});
const tr = container.querySelector('tr');
- const style = tr?.getAttribute('style') ?? '';
- expect(style).toContain('box-shadow');
- expect(style).toContain('#ef4444');
+ expect(tr).toHaveAttribute('data-workload-alert-accent', 'critical');
+ expect(tr?.getAttribute('style')).toBeNull();
});
- it('applies grey accent for acknowledged-only alerts', () => {
+ it('marks acknowledged-only alerts with the canonical alert accent tone', () => {
const { container } = renderGuestRow({
guest: makeGuest(),
alertStyles: {
@@ -425,8 +424,8 @@ describe('GuestRow', () => {
},
});
const tr = container.querySelector('tr');
- const style = tr?.getAttribute('style') ?? '';
- expect(style).toContain('#9ca3af');
+ expect(tr).toHaveAttribute('data-workload-alert-accent', 'acknowledged');
+ expect(tr?.getAttribute('style')).toBeNull();
});
});
diff --git a/frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx
index ece86dde7..8028856da 100644
--- a/frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx
+++ b/frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx
@@ -53,23 +53,15 @@ function getBarTrigger(container: HTMLElement): HTMLElement {
}
/** Get single-bar fill elements (non-stacked mode). */
-function getSingleBarFill(container: HTMLElement): HTMLElement | null {
- // Single bar is .absolute.top-0.left-0.h-full that is a direct child (not inside a flex container)
- const fills = container.querySelectorAll('.absolute.top-0.left-0.h-full');
- // In non-stacked mode, the single bar is not inside a .flex container
- for (const fill of fills) {
- if (!fill.parentElement?.classList.contains('flex')) {
- return fill;
- }
- }
- return fills[0] ?? null;
+function getSingleBarFill(container: HTMLElement): SVGRectElement | null {
+ return container.querySelector('rect[data-stacked-disk-fill="single"]');
}
/** Get stacked segment elements. */
-function getStackedSegments(container: HTMLElement): HTMLElement[] {
- const flexContainer = container.querySelector('.absolute.top-0.left-0.h-full.w-full.flex');
- if (!flexContainer) return [];
- return Array.from(flexContainer.children) as HTMLElement[];
+function getStackedSegments(container: HTMLElement): SVGRectElement[] {
+ return Array.from(
+ container.querySelectorAll('rect[data-stacked-disk-fill="segment"]'),
+ );
}
afterEach(() => {
@@ -105,7 +97,7 @@ describe('StackedDiskBar', () => {
const { container } = render(() => );
const bar = getSingleBarFill(container);
expect(bar).toBeInTheDocument();
- expect(bar).toHaveStyle({ width: '50%' });
+ expect(bar).toHaveAttribute('width', '50');
});
it('does not show stacked segments for a single disk', () => {
@@ -179,18 +171,15 @@ describe('StackedDiskBar', () => {
// Total capacity = 150 GiB
// disk1 used = 20 GiB → 20/150 ≈ 13.33%
// disk2 used = 50 GiB → 50/150 ≈ 33.33%
- const width1 = parseFloat(segments[0].style.width);
- const width2 = parseFloat(segments[1].style.width);
+ const width1 = parseFloat(segments[0].getAttribute('width') ?? '0');
+ const width2 = parseFloat(segments[1].getAttribute('width') ?? '0');
expect(width1).toBeCloseTo(13.33, 0);
expect(width2).toBeCloseTo(33.33, 0);
});
- it('adds border-right separator between segments', () => {
+ it('adds separators between stacked segments', () => {
const { container } = render(() => );
- const segments = getStackedSegments(container);
- expect(segments[0].style.borderRight).toContain('1px solid');
- // Last segment has border-right: 'none' in source, which jsdom omits from style
- expect(segments[1].style.borderRight).not.toContain('1px solid');
+ expect(container.querySelectorAll('line')).toHaveLength(1);
});
});
@@ -240,26 +229,23 @@ describe('StackedDiskBar', () => {
expect(screen.getByText('Disk 1')).toBeInTheDocument();
});
- it('renders grid layout with correct column count', () => {
+ it('renders one equal-width mini column per disk', () => {
const disk1 = makeDisk({ mountpoint: '/boot' });
const disk2 = makeDisk({ mountpoint: '/data' });
const disk3 = makeDisk({ mountpoint: '/var' });
const { container } = render(() => (
));
- const grid = container.querySelector('.grid');
- expect(grid).toBeInTheDocument();
- expect(grid).toHaveStyle({
- 'grid-template-columns': 'repeat(3, minmax(0, 1fr))',
- });
+ const columns = container.querySelectorAll('.flex.min-w-0.flex-1.flex-col.items-stretch.gap-0\\.5');
+ expect(columns).toHaveLength(3);
});
it('clamps mini bar fill width to exactly 100% when used exceeds total', () => {
const disk = makeDisk({ used: 200000000000, total: 107374182400, mountpoint: '/full' });
const { container } = render(() => );
- const miniBars = container.querySelectorAll('.h-full');
- const barFill = miniBars[miniBars.length - 1] as HTMLElement;
- expect(barFill.style.width).toBe('100%');
+ const miniBars = container.querySelectorAll('rect[data-stacked-disk-fill="mini"]');
+ const barFill = miniBars[miniBars.length - 1];
+ expect(barFill).toHaveAttribute('width', '100');
});
});
@@ -325,7 +311,7 @@ describe('StackedDiskBar', () => {
const bar = getSingleBarFill(container);
expect(bar).toBeInTheDocument();
// Normal green: rgba(34, 197, 94, 0.6)
- expect(bar!.style.backgroundColor).toContain('34, 197, 94');
+ expect(bar?.getAttribute('fill')).toContain('34, 197, 94');
});
it('uses warning color for disk at 80-89%', () => {
@@ -337,7 +323,7 @@ describe('StackedDiskBar', () => {
const bar = getSingleBarFill(container);
expect(bar).toBeInTheDocument();
// Warning yellow: rgba(234, 179, 8, 0.6)
- expect(bar!.style.backgroundColor).toContain('234, 179, 8');
+ expect(bar?.getAttribute('fill')).toContain('234, 179, 8');
});
it('uses critical color for disk at 90%+', () => {
@@ -349,7 +335,7 @@ describe('StackedDiskBar', () => {
const bar = getSingleBarFill(container);
expect(bar).toBeInTheDocument();
// Critical red: rgba(239, 68, 68, 0.6)
- expect(bar!.style.backgroundColor).toContain('239, 68, 68');
+ expect(bar?.getAttribute('fill')).toContain('239, 68, 68');
});
it('uses warning color for stacked segment at 80-89%', () => {
@@ -367,9 +353,9 @@ describe('StackedDiskBar', () => {
const segments = getStackedSegments(container);
expect(segments.length).toBe(2);
// First segment: normal → palette color (green)
- expect(segments[0].style.backgroundColor).toContain('34, 197, 94');
+ expect(segments[0].getAttribute('fill')).toContain('34, 197, 94');
// Second segment: warning → yellow
- expect(segments[1].style.backgroundColor).toContain('234, 179, 8');
+ expect(segments[1].getAttribute('fill')).toContain('234, 179, 8');
});
it('uses critical color for stacked segment at 90%+', () => {
@@ -387,9 +373,9 @@ describe('StackedDiskBar', () => {
const segments = getStackedSegments(container);
expect(segments.length).toBe(2);
// First segment: normal → palette color
- expect(segments[0].style.backgroundColor).not.toContain('239, 68, 68');
+ expect(segments[0].getAttribute('fill')).not.toContain('239, 68, 68');
// Second segment: critical → red
- expect(segments[1].style.backgroundColor).toContain('239, 68, 68');
+ expect(segments[1].getAttribute('fill')).toContain('239, 68, 68');
});
});
@@ -404,7 +390,7 @@ describe('StackedDiskBar', () => {
const { container } = render(() => );
const bar = getSingleBarFill(container);
expect(bar).toBeInTheDocument();
- expect(bar!.style.width).toBe('100%');
+ expect(bar).toHaveAttribute('width', '100');
});
});
@@ -493,7 +479,7 @@ describe('StackedDiskBar', () => {
const bar = getSingleBarFill(container);
expect(bar).toBeInTheDocument();
// Bar color should be critical (based on max disk at 92%)
- expect(bar!.style.backgroundColor).toContain('239, 68, 68');
+ expect(bar?.getAttribute('fill')).toContain('239, 68, 68');
});
});
});
diff --git a/frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts b/frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts
index b3e62c999..51134197e 100644
--- a/frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts
+++ b/frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts
@@ -1,6 +1,6 @@
import type { AnomalyReport } from '@/types/aiIntelligence';
import { ANOMALY_SEVERITY_CLASS, formatAnomalyRatio, formatPercent } from '@/utils/format';
-import { getMetricColorClass } from '@/utils/metricThresholds';
+import { getMetricColorClass, getMetricColorRgba } from '@/utils/metricThresholds';
export interface EnhancedCPUBarProps {
usage: number;
@@ -16,6 +16,7 @@ export interface EnhancedCPUBarPresentation {
anomalyDescription?: string;
anomalyRatio: string;
barClass: string;
+ barFill: string;
barWidth: string;
displayLoadAverage?: string;
displayUsage: string;
@@ -35,6 +36,7 @@ export function buildEnhancedCPUBarPresentation(
anomalyDescription: props.anomaly?.description,
anomalyRatio,
barClass: getMetricColorClass(props.usage, 'cpu'),
+ barFill: getMetricColorRgba(props.usage, 'cpu'),
barWidth: `${Math.min(props.usage, 100)}%`,
displayLoadAverage:
props.loadAverage !== undefined ? props.loadAverage.toFixed(2) : undefined,
diff --git a/frontend-modern/src/components/Dashboard/useGuestRowState.ts b/frontend-modern/src/components/Dashboard/useGuestRowState.ts
index 20326c1f9..918069746 100644
--- a/frontend-modern/src/components/Dashboard/useGuestRowState.ts
+++ b/frontend-modern/src/components/Dashboard/useGuestRowState.ts
@@ -216,12 +216,12 @@ export function useGuestRowState(props: GuestRowProps) {
() => hasUnacknowledgedAlert() || hasAcknowledgedOnlyAlert(),
);
- const alertAccentColor = createMemo(() => {
+ const alertAccentTone = createMemo<'critical' | 'warning' | 'acknowledged' | undefined>(() => {
if (!showAlertHighlight()) return undefined;
if (hasUnacknowledgedAlert()) {
- return props.alertStyles?.severity === 'critical' ? '#ef4444' : '#eab308';
+ return props.alertStyles?.severity === 'critical' ? 'critical' : 'warning';
}
- return '#9ca3af';
+ return 'acknowledged';
});
const rowClass = createMemo(() => {
@@ -243,17 +243,6 @@ export function useGuestRowState(props: GuestRowProps) {
return [base, hover, defaultHover, alertBg, stoppedDimming].filter(Boolean).join(' ');
});
- const rowStyle = createMemo(() => {
- const styles: Record = { 'min-height': '32px' };
- if (showAlertHighlight()) {
- const color = alertAccentColor();
- if (color) {
- styles['box-shadow'] = `inset 4px 0 0 0 ${color}`;
- }
- }
- return styles;
- });
-
const firstCellIndent = createMemo(() =>
props.isGroupedView ? GROUPED_FIRST_CELL_INDENT : DEFAULT_FIRST_CELL_INDENT,
);
@@ -301,6 +290,7 @@ export function useGuestRowState(props: GuestRowProps) {
ociImage,
osName,
osVersion,
+ alertAccentTone,
supportsBackup,
typeInfo,
workloadType,
@@ -308,6 +298,5 @@ export function useGuestRowState(props: GuestRowProps) {
clusterName,
agentVersion,
rowClass,
- rowStyle,
};
}
diff --git a/frontend-modern/src/components/shared/Table.tsx b/frontend-modern/src/components/shared/Table.tsx
index f9075d9b7..2b57c5fc2 100644
--- a/frontend-modern/src/components/shared/Table.tsx
+++ b/frontend-modern/src/components/shared/Table.tsx
@@ -18,8 +18,7 @@ export function Table(props: TableProps) {
{
expect(pulseDataGridModelSource).toContain('target.closest(');
expect(tableSource).toContain('customDividePattern');
expect(tableSource).toContain('customBorderPattern');
+ expect(tableSource).toContain('touch-scroll');
+ expect(tableSource).not.toContain('style={{');
+ expect(tableSource).not.toContain('style={');
});
it('triggers the row handler when a non-interactive cell is clicked', () => {
diff --git a/frontend-modern/src/components/shared/__tests__/Table.test.tsx b/frontend-modern/src/components/shared/__tests__/Table.test.tsx
index 52d9def9a..d508c1599 100644
--- a/frontend-modern/src/components/shared/__tests__/Table.test.tsx
+++ b/frontend-modern/src/components/shared/__tests__/Table.test.tsx
@@ -1,9 +1,16 @@
import { render, screen } from '@solidjs/testing-library';
import { describe, expect, it } from 'vitest';
+import tableSource from '@/components/shared/Table.tsx?raw';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/shared/Table';
describe('TableBody', () => {
+ it('keeps the shared table wrapper CSP-safe', () => {
+ expect(tableSource).toContain('touch-scroll');
+ expect(tableSource).not.toContain('style={{');
+ expect(tableSource).not.toContain('style={');
+ });
+
it('keeps default dividers when no custom divider classes are provided', () => {
render(() => (
diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css
index 8f5a78cfe..73f6ea5a9 100644
--- a/frontend-modern/src/index.css
+++ b/frontend-modern/src/index.css
@@ -173,6 +173,131 @@
@apply border-b border-border hover:bg-surface-hover;
}
+ .workload-table {
+ table-layout: fixed;
+ min-width: max-content;
+ }
+
+ .workload-table--mobile {
+ min-width: 100%;
+ }
+
+ .workload-table [data-workload-col='name'] {
+ width: 200px;
+ min-width: 200px;
+ max-width: 200px;
+ }
+
+ .workload-table [data-workload-col='info'] {
+ width: 100px;
+ min-width: 100px;
+ max-width: 100px;
+ }
+
+ .workload-table [data-workload-col='vmid'],
+ .workload-table [data-workload-col='ip'],
+ .workload-table [data-workload-col='os'] {
+ width: 45px;
+ min-width: 45px;
+ max-width: 45px;
+ }
+
+ .workload-table [data-workload-col='cpu'],
+ .workload-table [data-workload-col='memory'],
+ .workload-table [data-workload-col='disk'] {
+ width: 140px;
+ min-width: 140px;
+ max-width: 140px;
+ }
+
+ .workload-table [data-workload-col='uptime'],
+ .workload-table [data-workload-col='tags'],
+ .workload-table [data-workload-col='update'] {
+ width: 60px;
+ min-width: 60px;
+ max-width: 60px;
+ }
+
+ .workload-table [data-workload-col='node'] {
+ width: 70px;
+ min-width: 70px;
+ max-width: 70px;
+ }
+
+ .workload-table [data-workload-col='image'] {
+ width: 140px;
+ min-width: 120px;
+ max-width: 140px;
+ }
+
+ .workload-table [data-workload-col='namespace'] {
+ width: 110px;
+ min-width: 90px;
+ max-width: 110px;
+ }
+
+ .workload-table [data-workload-col='context'] {
+ width: 120px;
+ min-width: 100px;
+ max-width: 120px;
+ }
+
+ .workload-table [data-workload-col='backup'] {
+ width: 50px;
+ min-width: 50px;
+ max-width: 50px;
+ }
+
+ .workload-table [data-workload-col='netIo'],
+ .workload-table [data-workload-col='diskIo'] {
+ width: 130px;
+ min-width: 120px;
+ max-width: 130px;
+ }
+
+ .workload-table [data-workload-col='link'] {
+ width: 28px;
+ min-width: 28px;
+ max-width: 28px;
+ }
+
+ .workload-table--mobile [data-workload-col='name'] {
+ width: auto;
+ min-width: 120px;
+ max-width: none;
+ }
+
+ .workload-table--mobile [data-workload-col='cpu'],
+ .workload-table--mobile [data-workload-col='memory'],
+ .workload-table--mobile [data-workload-col='disk'] {
+ width: 70px;
+ min-width: 60px;
+ max-width: 70px;
+ }
+
+ .workload-table--mobile [data-workload-col='netIo'],
+ .workload-table--mobile [data-workload-col='diskIo'] {
+ width: 170px;
+ min-width: 170px;
+ max-width: 170px;
+ }
+
+ .workload-row {
+ min-height: 32px;
+ }
+
+ .workload-row[data-workload-alert-accent='critical'] > td:first-child {
+ box-shadow: inset 4px 0 0 0 #ef4444;
+ }
+
+ .workload-row[data-workload-alert-accent='warning'] > td:first-child {
+ box-shadow: inset 4px 0 0 0 #eab308;
+ }
+
+ .workload-row[data-workload-alert-accent='acknowledged'] > td:first-child {
+ box-shadow: inset 4px 0 0 0 #9ca3af;
+ }
+
tr[data-summary-group-member-active='preview'] > td,
tr[data-summary-group-member-active='preview'] > th {
background-color: var(--color-summary-group-member-preview-bg);