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: