diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
index d2be159cb9..8f29ee6b3e 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -26,62 +26,65 @@ regression protection.
4. `internal/api/slo.go`
5. `internal/api/slo_bench_test.go`
6. `frontend-modern/src/components/Dashboard/Dashboard.tsx`
-7. `frontend-modern/src/components/Dashboard/useDashboardState.ts`
-8. `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts`
-9. `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`
-10. `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
-11. `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts`
-12. `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`
-13. `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
-14. `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts`
-15. `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts`
-16. `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
-17. `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts`
-18. `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts`
-19. `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
-20. `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts`
-21. `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`
-22. `frontend-modern/src/components/Dashboard/MetricBar.tsx`
-23. `frontend-modern/src/components/Dashboard/metricBarModel.ts`
-24. `frontend-modern/src/components/Dashboard/useMetricBarState.ts`
-25. `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
-26. `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts`
-27. `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts`
-28. `frontend-modern/src/components/Dashboard/DiskList.tsx`
-29. `frontend-modern/src/components/Dashboard/diskListModel.ts`
-30. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
-31. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
-32. `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`
-33. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
-34. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
-35. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
-36. `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`
-37. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
-38. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
-39. `frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts`
-40. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
-41. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
-42. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
-43. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
-44. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
-45. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
-46. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
-47. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
-48. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx`
-49. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx`
-50. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx`
-51. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx`
-52. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
-53. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
-54. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
-55. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
-56. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
-57. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
-58. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
-59. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
-60. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
-61. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts`
-62. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
+7. `frontend-modern/src/components/Dashboard/DashboardStateCards.tsx`
+8. `frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx`
+9. `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`
+10. `frontend-modern/src/components/Dashboard/useDashboardState.ts`
+11. `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts`
+12. `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`
+13. `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
+14. `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts`
+15. `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`
+16. `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
+17. `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts`
+18. `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts`
+19. `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
+20. `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts`
+21. `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts`
+22. `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
+23. `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts`
+24. `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`
+25. `frontend-modern/src/components/Dashboard/MetricBar.tsx`
+26. `frontend-modern/src/components/Dashboard/metricBarModel.ts`
+27. `frontend-modern/src/components/Dashboard/useMetricBarState.ts`
+28. `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
+29. `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts`
+30. `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts`
+31. `frontend-modern/src/components/Dashboard/DiskList.tsx`
+32. `frontend-modern/src/components/Dashboard/diskListModel.ts`
+33. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
+34. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
+35. `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`
+36. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
+37. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
+38. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
+39. `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`
+40. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
+41. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
+42. `frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts`
+43. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
+44. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
+45. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
+46. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
+47. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
+48. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
+49. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
+50. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
+51. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx`
+52. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx`
+53. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx`
+54. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx`
+55. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
+56. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
+57. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
+58. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
+59. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
+60. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
+61. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
+62. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
+63. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
+64. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts`
+65. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
## Shared Boundaries
@@ -113,6 +116,7 @@ regression protection.
18. 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`
19. Extend enhanced CPU bar usage/anomaly presentation and tooltip runtime through `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts` and `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts` rather than rebuilding tooltip-local state and CPU-threshold formatting inside `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
20. Extend grouped dashboard row windowing, reveal-index clamping, overscan math, and per-group visible-slice derivation through `frontend-modern/src/components/Dashboard/useGroupedTableWindowing.ts` rather than rebuilding scroll handlers, mounted-row budgets, or group-slice math inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
+21. Extend dashboard shell rendering through `frontend-modern/src/components/Dashboard/DashboardStateCards.tsx`, `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx`, and `frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx` rather than accreting loading cards, workload table markup, or stats-strip presentation back into `frontend-modern/src/components/Dashboard/Dashboard.tsx`
## Forbidden Paths
diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json
index af203a911d..f9c7497a30 100644
--- a/docs/release-control/v6/internal/subsystems/registry.json
+++ b/docs/release-control/v6/internal/subsystems/registry.json
@@ -2556,6 +2556,9 @@
"frontend-modern/src/components/Dashboard/Dashboard.tsx",
"frontend-modern/src/components/Dashboard/DashboardFilter.tsx",
"frontend-modern/src/components/Dashboard/dashboardFilterModel.ts",
+ "frontend-modern/src/components/Dashboard/DashboardStateCards.tsx",
+ "frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx",
+ "frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx",
"frontend-modern/src/components/Dashboard/DiskList.tsx",
"frontend-modern/src/components/Dashboard/diskListModel.ts",
"frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx",
@@ -2647,6 +2650,9 @@
"frontend-modern/src/components/Dashboard/Dashboard.tsx",
"frontend-modern/src/components/Dashboard/DashboardFilter.tsx",
"frontend-modern/src/components/Dashboard/dashboardFilterModel.ts",
+ "frontend-modern/src/components/Dashboard/DashboardStateCards.tsx",
+ "frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx",
+ "frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx",
"frontend-modern/src/components/Dashboard/DiskList.tsx",
"frontend-modern/src/components/Dashboard/diskListModel.ts",
"frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx",
diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx
index c04c1a7e53..b91a5f2851 100644
--- a/frontend-modern/src/components/Dashboard/Dashboard.tsx
+++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx
@@ -1,511 +1,137 @@
-import { createMemo, For, Index, Show } from 'solid-js';
-import { GuestRow } from './GuestRow';
-import { GuestDrawer } from './GuestDrawer';
-import { getAlertStyles } from '@/utils/alerts';
-import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
+import { Show } from 'solid-js';
import { InfrastructureSelector } from '@/components/shared/InfrastructureSelector';
import { DashboardFilter } from './DashboardFilter';
-import { Card } from '@/components/shared/Card';
-import {
- Table,
- TableHeader,
- TableBody,
- TableRow,
- TableHead,
- TableCell,
-} from '@/components/shared/Table';
-import { EmptyState } from '@/components/shared/EmptyState';
-import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
-import { isNodeOnline } from '@/utils/status';
-import { getCanonicalWorkloadId } from '@/utils/workloads';
import { WorkloadsSummary } from '@/components/Workloads/WorkloadsSummary';
import { ScrollToTopButton } from '@/components/shared/ScrollToTopButton';
-import { useDashboardState, type DashboardProps, type WorkloadSortKey } from './useDashboardState';
+import { DashboardStateCards } from './DashboardStateCards';
+import { DashboardStatsStrip } from './DashboardStatsStrip';
+import { DashboardWorkloadTable } from './DashboardWorkloadTable';
+import { useDashboardState, type DashboardProps } from './useDashboardState';
export function Dashboard(props: DashboardProps) {
- const {
- activeAlerts,
- alertsEnabled,
- allGuests,
- bottomSpacerHeight,
- connected,
- containerRuntimeFilterConfig,
- dashboardFilterColumnVisibility,
- dashboardDisconnectedState,
- dashboardGuestsEmptyState,
- dashboardInfrastructureEmptyState,
- dashboardLoadingState,
- filteredGuests,
- getGroupLabel,
- groupedGuests,
- groupedWindowing,
- guestMetadata,
- guestParentNodeMap,
- handleBeforeAutoFocus,
- handleCustomUrlUpdate,
- handleNodeSelect,
- handleSort,
- handleTagClick,
- hostFilterConfig,
- hoveredWorkloadId,
- initialDataReceived,
- isMobile,
- isWorkloadsRoute,
- kioskMode,
- mobileVisibleColumnIds,
- mobileVisibleColumns,
- navigate,
- namespaceFilterConfig,
- nodeByInstance,
- reconnect,
- search,
- selectedGuestId,
- selectedNode,
- setGroupingMode,
- setHoveredWorkloadId,
- setSearch,
- setSelectedGuestId,
- setSortDirection,
- setSortKey,
- setStatusMode,
- setTableBodyRef,
- setTableWrapperRef,
- setViewMode,
- setWorkloadsSummaryCollapsed,
- setWorkloadsSummaryRange,
- sortDirection,
- sortKey,
- statusMode,
- topSpacerHeight,
- totalColumns,
- totalStats,
- viewMode,
- visibleColumns,
- visibleGroupKeys,
- windowedGroupedGuests,
- workloadIOEmphasis,
- workloads,
- workloadsSummaryCollapsed,
- workloadsSummaryFallbackCounts,
- workloadsSummaryFallbackSnapshots,
- workloadsSummaryRange,
- workloadsSummaryVisibleIds,
- ws,
- groupingMode,
- } = useDashboardState(props);
+ const state = useDashboardState(props);
return (
-
+
- {/* Infrastructure selector - infrastructure summary (hidden on workloads) */}
- {/* Loading State */}
-
-
-
-
-
-
- }
- title={dashboardLoadingState().title}
- description={dashboardLoadingState().description}
- />
-
-
+
- {/* Empty state when no infrastructure has connected yet */}
0
}
>
-
-
-
-
- }
- title={dashboardInfrastructureEmptyState().title}
- description={dashboardInfrastructureEmptyState().description}
- actions={
- !kioskMode() ? (
-
- ) : undefined
- }
- />
-
-
-
- {/* Disconnected State */}
-
-
-
-
-
- }
- title={dashboardDisconnectedState().title}
- description={dashboardDisconnectedState().description}
- tone="danger"
- actions={
- dashboardDisconnectedState().actionLabel ? (
-
- ) : undefined
- }
- />
-
-
-
- {/* Dashboard Filter - hidden in kiosk mode */}
- 0}>
setWorkloadsSummaryCollapsed((c) => !c) : undefined
+ state.isWorkloadsRoute()
+ ? () => state.setWorkloadsSummaryCollapsed((collapsed) => !collapsed)
+ : undefined
}
- containerRuntimeFilter={containerRuntimeFilterConfig()}
- hostFilter={hostFilterConfig()}
- namespaceFilter={namespaceFilterConfig()}
+ containerRuntimeFilter={state.containerRuntimeFilterConfig()}
+ hostFilter={state.hostFilterConfig()}
+ namespaceFilter={state.namespaceFilterConfig()}
/>
- {/* Table View */}
- 0}>
-
-
-
- Workloads
-
-
-
-
-
-
- {(col) => {
- const isFirst = () => col.id === visibleColumns()[0]?.id;
- const sortKeyForCol = col.sortKey as WorkloadSortKey | undefined;
- const isSortable = !!sortKeyForCol;
- const isSorted = () => sortKeyForCol && sortKey() === sortKeyForCol;
-
- return (
- isSortable && handleSort(sortKeyForCol!)}
- title={col.icon ? col.label : undefined}
- >
-
- {col.icon ? (
- {col.icon}
- ) : (
- col.label
- )}
- {isSorted() && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
-
-
- );
- }}
-
-
-
-
- 0}>
-
-
-
-
- {/* Outer uses string keys — strings compare by value so DOM is stable across data updates */}
- >}>
- {(groupKey) => {
- const groupGuests = () => windowedGroupedGuests()[groupKey] || [];
- const fullGroupGuests = () => groupedGuests()[groupKey] || [];
- const node = () => nodeByInstance()[groupKey];
- return (
- <>
-
-
-
- {(() => {
- const label = getGroupLabel(groupKey, fullGroupGuests());
- return (
-
- {label.name}
-
-
- {label.type}
-
-
-
- );
- })()}
-
-
- }
- >
-
-
-
- {/* Inner tracks by position — updates props reactively instead of recreating DOM */}
- >}>
- {(guest) => {
- const guestId = createMemo(() => getCanonicalWorkloadId(guest()));
- const getMetadata = () =>
- guestMetadata()[guestId()] ||
- guestMetadata()[
- `${guest().instance}:${guest().node}:${guest().vmid}`
- ];
- const parentNode = () => node() ?? guestParentNodeMap()[guestId()];
- const parentNodeOnline = () => {
- const pn = parentNode();
- return pn ? isNodeOnline(pn) : true;
- };
-
- return (
-
-
- setSelectedGuestId(
- selectedGuestId() === guestId() ? null : guestId(),
- )
- }
- isExpanded={selectedGuestId() === guestId()}
- ioEmphasis={workloadIOEmphasis()}
- onHoverChange={setHoveredWorkloadId}
- />
-
-
-
- e.stopPropagation()}
- >
- setSelectedGuestId(null)}
- customUrl={getMetadata()?.customUrl}
- onCustomUrlChange={handleCustomUrlUpdate}
- />
-
-
-
-
-
- );
- }}
-
- >
- );
- }}
-
- 0}>
-
-
-
-
-
-
-
-
-
-
-
0
+ state.connected() &&
+ state.initialDataReceived() &&
+ state.filteredGuests().length > 0
}
>
-
-
-
-
- }
- title={dashboardGuestsEmptyState().title}
- description={dashboardGuestsEmptyState().description}
- />
-
+
- {/* Stats */}
-
-
-
-
-
- {totalStats().running} running
-
- 0}>
- |
-
-
- {totalStats().degraded} degraded
-
-
- |
-
-
- {totalStats().stopped} stopped
-
-
-
-
+
diff --git a/frontend-modern/src/components/Dashboard/DashboardStateCards.tsx b/frontend-modern/src/components/Dashboard/DashboardStateCards.tsx
new file mode 100644
index 0000000000..2e5185e7b3
--- /dev/null
+++ b/frontend-modern/src/components/Dashboard/DashboardStateCards.tsx
@@ -0,0 +1,169 @@
+import { Show } from 'solid-js';
+
+import { Card } from '@/components/shared/Card';
+import { EmptyState } from '@/components/shared/EmptyState';
+
+import type { DashboardState } from './useDashboardState';
+
+type DashboardStateCardsProps = Pick<
+ DashboardState,
+ | 'allGuests'
+ | 'connected'
+ | 'dashboardDisconnectedState'
+ | 'dashboardGuestsEmptyState'
+ | 'dashboardInfrastructureEmptyState'
+ | 'dashboardLoadingState'
+ | 'filteredGuests'
+ | 'initialDataReceived'
+ | 'kioskMode'
+ | 'navigate'
+ | 'reconnect'
+ | 'workloads'
+> & {
+ nodeCount: number;
+};
+
+export function DashboardStateCards(props: DashboardStateCardsProps) {
+ return (
+ <>
+
+
+
+
+
+
+ }
+ title={props.dashboardLoadingState().title}
+ description={props.dashboardLoadingState().description}
+ />
+
+
+
+
+
+
+
+
+ }
+ title={props.dashboardInfrastructureEmptyState().title}
+ description={props.dashboardInfrastructureEmptyState().description}
+ actions={
+ !props.kioskMode() ? (
+
+ ) : undefined
+ }
+ />
+
+
+
+
+
+
+
+
+ }
+ title={props.dashboardDisconnectedState().title}
+ description={props.dashboardDisconnectedState().description}
+ tone="danger"
+ actions={
+ props.dashboardDisconnectedState().actionLabel ? (
+
+ ) : undefined
+ }
+ />
+
+
+
+ 0
+ }
+ >
+
+
+
+
+ }
+ title={props.dashboardGuestsEmptyState().title}
+ description={props.dashboardGuestsEmptyState().description}
+ />
+
+
+ >
+ );
+}
diff --git a/frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx b/frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx
new file mode 100644
index 0000000000..0f9ca817f0
--- /dev/null
+++ b/frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx
@@ -0,0 +1,35 @@
+import { Show } from 'solid-js';
+
+import type { DashboardState } from './useDashboardState';
+
+type DashboardStatsStripProps = Pick<
+ DashboardState,
+ 'connected' | 'initialDataReceived' | 'totalStats'
+>;
+
+export function DashboardStatsStrip(props: DashboardStatsStripProps) {
+ return (
+
+
+
+
+
+ {props.totalStats().running} running
+
+ 0}>
+ |
+
+
+ {props.totalStats().degraded} degraded
+
+
+ |
+
+
+ {props.totalStats().stopped} stopped
+
+
+
+
+ );
+}
diff --git a/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx b/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx
new file mode 100644
index 0000000000..68c2740af9
--- /dev/null
+++ b/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx
@@ -0,0 +1,250 @@
+import { createMemo, For, Index, Show } from 'solid-js';
+
+import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
+import { Card } from '@/components/shared/Card';
+import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/shared/Table';
+import { getAlertStyles } from '@/utils/alerts';
+import { isNodeOnline } from '@/utils/status';
+import { getCanonicalWorkloadId } from '@/utils/workloads';
+
+import { GuestDrawer } from './GuestDrawer';
+import { GuestRow } from './GuestRow';
+import type { DashboardState, WorkloadSortKey } from './useDashboardState';
+
+type DashboardWorkloadTableProps = Pick<
+ DashboardState,
+ | 'activeAlerts'
+ | 'alertsEnabled'
+ | 'bottomSpacerHeight'
+ | 'getGroupLabel'
+ | 'groupedGuests'
+ | 'groupedWindowing'
+ | 'guestMetadata'
+ | 'guestParentNodeMap'
+ | 'groupingMode'
+ | 'handleCustomUrlUpdate'
+ | 'handleSort'
+ | 'handleTagClick'
+ | 'isMobile'
+ | 'mobileVisibleColumnIds'
+ | 'mobileVisibleColumns'
+ | 'nodeByInstance'
+ | 'search'
+ | 'selectedGuestId'
+ | 'setHoveredWorkloadId'
+ | 'setSelectedGuestId'
+ | 'setTableBodyRef'
+ | 'setTableWrapperRef'
+ | 'sortDirection'
+ | 'sortKey'
+ | 'topSpacerHeight'
+ | 'totalColumns'
+ | 'visibleColumns'
+ | 'visibleGroupKeys'
+ | 'windowedGroupedGuests'
+ | 'workloadIOEmphasis'
+>;
+
+export function DashboardWorkloadTable(props: DashboardWorkloadTableProps) {
+ return (
+
+
+
+ Workloads
+
+
+
+
+
+
+ {(col) => {
+ const isFirst = () => col.id === props.visibleColumns()[0]?.id;
+ const sortKeyForCol = col.sortKey as WorkloadSortKey | undefined;
+ const isSortable = !!sortKeyForCol;
+ const isSorted = () => sortKeyForCol && props.sortKey() === sortKeyForCol;
+
+ return (
+ isSortable && props.handleSort(sortKeyForCol!)}
+ title={col.icon ? col.label : undefined}
+ >
+
+ {col.icon ? (
+ {col.icon}
+ ) : (
+ col.label
+ )}
+ {isSorted() && (props.sortDirection() === 'asc' ? ' ▲' : ' ▼')}
+
+
+ );
+ }}
+
+
+
+
+ 0}>
+
+
+
+
+ >}>
+ {(groupKey) => {
+ const groupGuests = () => props.windowedGroupedGuests()[groupKey] || [];
+ const fullGroupGuests = () => props.groupedGuests()[groupKey] || [];
+ const node = () => props.nodeByInstance()[groupKey];
+
+ return (
+ <>
+
+
+
+ {(() => {
+ const label = props.getGroupLabel(groupKey, fullGroupGuests());
+ return (
+
+ {label.name}
+
+
+ {label.type}
+
+
+
+ );
+ })()}
+
+
+ }
+ >
+
+
+
+ >}>
+ {(guest) => {
+ const guestId = createMemo(() => getCanonicalWorkloadId(guest()));
+ const metadata = () =>
+ props.guestMetadata()[guestId()] ||
+ props.guestMetadata()[
+ `${guest().instance}:${guest().node}:${guest().vmid}`
+ ];
+ const parentNode = () =>
+ node() ?? props.guestParentNodeMap()[guestId()];
+ const parentNodeOnline = () => {
+ const pn = parentNode();
+ return pn ? isNodeOnline(pn) : true;
+ };
+
+ return (
+
+
+ props.setSelectedGuestId(
+ props.selectedGuestId() === guestId() ? null : guestId(),
+ )
+ }
+ isExpanded={props.selectedGuestId() === guestId()}
+ ioEmphasis={props.workloadIOEmphasis()}
+ onHoverChange={props.setHoveredWorkloadId}
+ />
+
+
+
+ event.stopPropagation()}
+ >
+ props.setSelectedGuestId(null)}
+ customUrl={metadata()?.customUrl}
+ onCustomUrlChange={props.handleCustomUrlUpdate}
+ />
+
+
+
+
+
+ );
+ }}
+
+ >
+ );
+ }}
+
+ 0}>
+
+
+
+
+
+
+
+
+
+ );
+}
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 f03835fdce..18a3a3d8ee 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
@@ -4,7 +4,10 @@ import { createSignal, onCleanup, onMount } from 'solid-js';
import type { Resource } from '@/types/resource';
import { Dashboard } from '../Dashboard';
import dashboardSource from '../Dashboard.tsx?raw';
+import dashboardStateCardsSource from '../DashboardStateCards.tsx?raw';
+import dashboardStatsStripSource from '../DashboardStatsStrip.tsx?raw';
import dashboardFilterSource from '../DashboardFilter.tsx?raw';
+import dashboardWorkloadTableSource from '../DashboardWorkloadTable.tsx?raw';
import dashboardFilterModelSource from '../dashboardFilterModel.ts?raw';
import dashboardGuestMetadataStateSource from '../useDashboardGuestMetadataState.ts?raw';
import dashboardWorkloadRouteStateSource from '../useDashboardWorkloadRouteState.ts?raw';
@@ -491,6 +494,9 @@ describe('Dashboard performance contract', () => {
it('keeps hot-path dashboard state in the shared dashboard state owner', () => {
expect(dashboardSource).toContain('useDashboardState');
+ expect(dashboardSource).toContain('DashboardStateCards');
+ expect(dashboardSource).toContain('DashboardStatsStrip');
+ expect(dashboardSource).toContain('DashboardWorkloadTable');
expect(dashboardSource).not.toContain('const [search, setSearch] = createSignal(');
expect(dashboardStateSource).toContain('useDashboardGuestMetadataState');
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
@@ -514,7 +520,10 @@ describe('Dashboard performance contract', () => {
expect(dashboardStateSource).not.toContain('const DEFAULT_WINDOW_SIZE =');
expect(dashboardStateSource).not.toContain('const DEFAULT_ENABLE_THRESHOLD =');
expect(dashboardStateSource).not.toContain('const DEFAULT_OVERSCAN_ROWS =');
- expect(dashboardSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
+ expect(dashboardSource).not.toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
+ expect(dashboardWorkloadTableSource).toContain(
+ 'createMemo(() => getCanonicalWorkloadId(guest()))',
+ );
expect(dashboardStateSource).not.toContain('const guestId = () => {');
});
@@ -640,6 +649,21 @@ describe('Dashboard performance contract', () => {
expect(guestDrawerOverviewSource).toContain('Filesystems');
});
+ it('keeps dashboard shell rendering in canonical section owners', () => {
+ expect(dashboardSource).toContain('DashboardStateCards');
+ expect(dashboardSource).toContain('DashboardStatsStrip');
+ expect(dashboardSource).toContain('DashboardWorkloadTable');
+ expect(dashboardSource).not.toContain('TableHeader');
+ expect(dashboardSource).not.toContain('NodeGroupHeader');
+ expect(dashboardStateCardsSource).toContain('dashboardInfrastructureEmptyState().title');
+ expect(dashboardStateCardsSource).toContain('dashboardDisconnectedState().actionLabel');
+ expect(dashboardWorkloadTableSource).toContain('TableHeader');
+ expect(dashboardWorkloadTableSource).toContain('NodeGroupHeader');
+ expect(dashboardWorkloadTableSource).toContain('GuestDrawer');
+ expect(dashboardStatsStripSource).toContain('totalStats().running');
+ expect(dashboardStatsStripSource).toContain('totalStats().stopped');
+ });
+
it('keeps disk-list runtime and derivations in canonical disk-list owners', () => {
expect(diskListSource).toContain('useDiskListState');
expect(diskListSource).not.toContain('const getUsagePercent =');
diff --git a/frontend-modern/src/components/Dashboard/useDashboardState.ts b/frontend-modern/src/components/Dashboard/useDashboardState.ts
index ff0f7793f7..44b86883e3 100644
--- a/frontend-modern/src/components/Dashboard/useDashboardState.ts
+++ b/frontend-modern/src/components/Dashboard/useDashboardState.ts
@@ -199,6 +199,7 @@ export function useDashboardState(props: DashboardProps) {
selectedKubernetesNamespace,
selectedNode,
setContainerRuntime,
+ setSelectedNode,
setSelectedKubernetesContext,
setSelectedKubernetesNamespace,
setViewMode,
@@ -665,3 +666,5 @@ export function useDashboardState(props: DashboardProps) {
groupingMode,
} as const;
}
+
+export type DashboardState = ReturnType;
diff --git a/frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts b/frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts
index 388a726183..9e2bb04a30 100644
--- a/frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts
+++ b/frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts
@@ -493,6 +493,7 @@ export function useDashboardWorkloadRouteState(options: DashboardWorkloadRouteSt
selectedKubernetesNamespace,
selectedNode,
setContainerRuntime,
+ setSelectedNode,
setSelectedKubernetesContext,
setSelectedKubernetesNamespace,
setViewMode,
diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts
index 27c667f973..b53d6203ce 100644
--- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts
+++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts
@@ -80,7 +80,10 @@ import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.t
import recoveryTimelineChartPresentationSource from '@/utils/recoveryTimelineChartPresentation.ts?raw';
import recoveryTimelinePresentationSource from '@/utils/recoveryTimelinePresentation.ts?raw';
import dashboardSource from '@/components/Dashboard/Dashboard.tsx?raw';
+import dashboardStateCardsSource from '@/components/Dashboard/DashboardStateCards.tsx?raw';
+import dashboardStatsStripSource from '@/components/Dashboard/DashboardStatsStrip.tsx?raw';
import dashboardFilterSource from '@/components/Dashboard/DashboardFilter.tsx?raw';
+import dashboardWorkloadTableSource from '@/components/Dashboard/DashboardWorkloadTable.tsx?raw';
import dashboardFilterModelSource from '@/components/Dashboard/dashboardFilterModel.ts?raw';
import dashboardGuestMetadataStateSource from '@/components/Dashboard/useDashboardGuestMetadataState.ts?raw';
import dashboardWorkloadRouteStateSource from '@/components/Dashboard/useDashboardWorkloadRouteState.ts?raw';
@@ -509,13 +512,19 @@ describe('frontend resource type boundaries', () => {
expect(orgScopeSource).toContain("export const DEFAULT_ORG_SCOPE = 'default'");
expect(orgScopeSource).toContain('export const normalizeOrgScope');
expect(dashboardSource).toContain('useDashboardState');
+ expect(dashboardSource).toContain('DashboardStateCards');
+ expect(dashboardSource).toContain('DashboardStatsStrip');
+ expect(dashboardSource).toContain('DashboardWorkloadTable');
expect(dashboardSource).not.toContain('const [search, setSearch] = createSignal(');
expect(dashboardStateSource).toContain('useDashboardGuestMetadataState');
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
expect(dashboardWorkloadRouteStateSource).toContain('normalizeWorkloadViewModeParam');
expect(dashboardSource).not.toContain('function normalizeViewModeParam');
expect(dashboardSource).not.toContain('workloadSummaryGuestId');
- expect(dashboardSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
+ expect(dashboardSource).not.toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
+ expect(dashboardWorkloadTableSource).toContain(
+ 'createMemo(() => getCanonicalWorkloadId(guest()))',
+ );
expect(dashboardGuestMetadataStateSource).toContain('normalizeOrgScope(getOrgID())');
expect(dashboardStateSource).not.toContain("const DEFAULT_ORG_SCOPE = 'default'");
expect(dashboardStateSource).not.toContain('const normalizeOrgScope =');
@@ -634,6 +643,10 @@ describe('frontend resource type boundaries', () => {
expect(guestDrawerSource).not.toContain('WebInterfaceUrlField');
expect(guestDrawerOverviewSource).toContain('WebInterfaceUrlField');
expect(guestDrawerOverviewSource).toContain('DiskList');
+ expect(dashboardStateCardsSource).toContain('dashboardDisconnectedState().actionLabel');
+ expect(dashboardWorkloadTableSource).toContain('NodeGroupHeader');
+ expect(dashboardWorkloadTableSource).toContain('GuestDrawer');
+ expect(dashboardStatsStripSource).toContain('totalStats().running');
expect(emptyStateSource).toContain('getEmptyStatePresentation');
expect(emptyStateSource).not.toContain('const iconBgClass: Record =');
expect(emptyStateSource).not.toContain(
diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py
index 4f73591ec7..b1995b1c4a 100644
--- a/scripts/release_control/subsystem_lookup_test.py
+++ b/scripts/release_control/subsystem_lookup_test.py
@@ -1962,6 +1962,33 @@ class SubsystemLookupTest(unittest.TestCase):
"dashboard-workload-hot-path",
)
+ def test_lookup_paths_assigns_dashboard_workload_table_runtime_to_performance_and_scalability(
+ self,
+ ) -> None:
+ result = lookup_paths(
+ ["frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx"]
+ )
+ self.assertEqual(result["unowned_runtime_files"], [])
+ self.assertEqual(
+ {item["subsystem"] for item in result["impacted_subsystems"]},
+ {"performance-and-scalability"},
+ )
+ file_entry = result["files"][0]
+ 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["verification_requirement"]["id"],
+ "dashboard-workload-hot-path",
+ )
+
def test_lookup_paths_assigns_dashboard_grouped_windowing_runtime_to_performance_and_scalability(
self,
) -> None: