diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 41d72e28b..704d6ec6c 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -44,90 +44,90 @@ work extends shared components instead of creating new local variants. 22. `frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx` 23. `frontend-modern/src/components/Settings/OperationsPanel.tsx` 24. `frontend-modern/src/utils/diagnosticsPresentation.ts` -24. `frontend-modern/src/utils/discoveryPresentation.ts` -24. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` -25. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx` -26. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx` -27. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` -28. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` -29. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` -30. `frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts` -30. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx` -31. `frontend-modern/src/components/Settings/useAISettingsState.ts` -32. `frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts` -33. `frontend-modern/src/components/Settings/useSettingsShellState.ts` -34. `frontend-modern/src/components/Settings/useSSOProvidersState.ts` -35. `frontend-modern/src/components/Settings/ssoProvidersModel.ts` -36. `frontend-modern/src/utils/ssoProviderPresentation.ts` -37. `frontend-modern/src/utils/systemSettingsPresentation.ts` -38. `frontend-modern/src/utils/aiSettingsPresentation.ts` -39. `frontend-modern/src/utils/settingsShellPresentation.ts` -40. `frontend-modern/src/utils/textPresentation.ts` -37. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx` -38. `frontend-modern/src/components/Settings/updatesSettingsModel.ts` -39. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` -40. `frontend-modern/src/components/Settings/ReportingPanel.tsx` -41. `frontend-modern/src/components/Settings/reportingPanelModel.ts` -42. `frontend-modern/src/components/Settings/reportingInventoryExportModel.ts` -43. `frontend-modern/src/components/Settings/useReportingPanelState.ts` -44. `frontend-modern/src/utils/reportingPresentation.ts` -45. `frontend-modern/src/utils/updatesPresentation.ts` -44. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts` -45. `tests/integration/tests/15-settings-shell-consistency.spec.ts` -46. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts` -47. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` -48. `frontend-modern/src/features/` -49. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` -50. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` -50. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` -51. `frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx` -52. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx` -53. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx` -54. `frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx` -55. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx` -56. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx` -57. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts` -58. `frontend-modern/src/utils/systemLogsPresentation.ts` -59. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx` -60. `frontend-modern/src/features/operations/OperationsPageSurface.tsx` -61. `frontend-modern/src/features/operations/operationsPageModel.ts` -62. `frontend-modern/src/pages/Operations.tsx` -63. `frontend-modern/src/components/Settings/ResourcePicker.tsx` -64. `frontend-modern/src/components/Settings/reportingResourceTypes.ts` -65. `frontend-modern/src/utils/reportableResourceTypes.ts` -66. `frontend-modern/src/utils/reportingResourceTypes.ts` -67. `frontend-modern/src/utils/problemResourcePresentation.ts` -68. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts` -69. `frontend-modern/src/utils/dashboardGuestPresentation.ts` -70. `frontend-modern/src/utils/dashboardKpiPresentation.ts` -71. `frontend-modern/src/utils/dashboardTrendPresentation.ts` -72. `frontend-modern/src/components/Toast/Toast.tsx` -73. `frontend-modern/src/utils/toast.ts` -74. `frontend-modern/src/utils/semanticTonePresentation.ts` -75. `frontend-modern/src/utils/emptyStatePresentation.ts` -76. `frontend-modern/src/utils/typeColumnPresentation.ts` -77. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts` -78. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx` -79. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx` -80. `frontend-modern/src/components/Settings/networkSettingsModel.ts` -81. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` -82. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` -83. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` -84. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts` -85. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` -86. `frontend-modern/src/components/Settings/settingsNavCatalog.ts` -87. `frontend-modern/src/components/Settings/settingsNavVisibility.ts` -88. `frontend-modern/src/components/Settings/settingsRouting.ts` -89. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts` -90. `frontend-modern/src/components/Settings/settingsTypes.ts` -91. `frontend-modern/src/components/Settings/useSettingsNavigation.ts` -92. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` -93. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` -94. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` -95. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` -96. `frontend-modern/src/utils/environmentLockPresentation.ts` -97. `frontend-modern/src/utils/docsLinks.ts` -98. `tests/integration/tests/20-local-doc-links.spec.ts` +25. `frontend-modern/src/utils/discoveryPresentation.ts` +26. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` +27. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx` +28. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx` +29. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` +30. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` +31. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` +32. `frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts` +33. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx` +34. `frontend-modern/src/components/Settings/useAISettingsState.ts` +35. `frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts` +36. `frontend-modern/src/components/Settings/useSettingsShellState.ts` +37. `frontend-modern/src/components/Settings/useSSOProvidersState.ts` +38. `frontend-modern/src/components/Settings/ssoProvidersModel.ts` +39. `frontend-modern/src/utils/ssoProviderPresentation.ts` +40. `frontend-modern/src/utils/systemSettingsPresentation.ts` +41. `frontend-modern/src/utils/aiSettingsPresentation.ts` +42. `frontend-modern/src/utils/settingsShellPresentation.ts` +43. `frontend-modern/src/utils/textPresentation.ts` +44. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx` +45. `frontend-modern/src/components/Settings/updatesSettingsModel.ts` +46. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` +47. `frontend-modern/src/components/Settings/ReportingPanel.tsx` +48. `frontend-modern/src/components/Settings/reportingPanelModel.ts` +49. `frontend-modern/src/components/Settings/reportingInventoryExportModel.ts` +50. `frontend-modern/src/components/Settings/useReportingPanelState.ts` +51. `frontend-modern/src/utils/reportingPresentation.ts` +52. `frontend-modern/src/utils/updatesPresentation.ts` +53. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts` +54. `tests/integration/tests/15-settings-shell-consistency.spec.ts` +55. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts` +56. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` +57. `frontend-modern/src/features/` +58. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` +59. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` +60. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx` +61. `frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx` +62. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx` +63. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx` +64. `frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx` +65. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx` +66. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx` +67. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts` +68. `frontend-modern/src/utils/systemLogsPresentation.ts` +69. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx` +70. `frontend-modern/src/features/operations/OperationsPageSurface.tsx` +71. `frontend-modern/src/features/operations/operationsPageModel.ts` +72. `frontend-modern/src/pages/Operations.tsx` +73. `frontend-modern/src/components/Settings/ResourcePicker.tsx` +74. `frontend-modern/src/components/Settings/reportingResourceTypes.ts` +75. `frontend-modern/src/utils/reportableResourceTypes.ts` +76. `frontend-modern/src/utils/reportingResourceTypes.ts` +77. `frontend-modern/src/utils/problemResourcePresentation.ts` +78. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts` +79. `frontend-modern/src/utils/dashboardGuestPresentation.ts` +80. `frontend-modern/src/utils/dashboardKpiPresentation.ts` +81. `frontend-modern/src/utils/dashboardTrendPresentation.ts` +82. `frontend-modern/src/components/Toast/Toast.tsx` +83. `frontend-modern/src/utils/toast.ts` +84. `frontend-modern/src/utils/semanticTonePresentation.ts` +85. `frontend-modern/src/utils/emptyStatePresentation.ts` +86. `frontend-modern/src/utils/typeColumnPresentation.ts` +87. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts` +88. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx` +89. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx` +90. `frontend-modern/src/components/Settings/networkSettingsModel.ts` +91. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` +92. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` +93. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` +94. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts` +95. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` +96. `frontend-modern/src/components/Settings/settingsNavCatalog.ts` +97. `frontend-modern/src/components/Settings/settingsNavVisibility.ts` +98. `frontend-modern/src/components/Settings/settingsRouting.ts` +99. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts` +100. `frontend-modern/src/components/Settings/settingsTypes.ts` +101. `frontend-modern/src/components/Settings/useSettingsNavigation.ts` +102. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` +103. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` +104. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` +105. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` +106. `frontend-modern/src/utils/environmentLockPresentation.ts` +107. `frontend-modern/src/utils/docsLinks.ts` +108. `tests/integration/tests/20-local-doc-links.spec.ts` ## Shared Boundaries @@ -200,86 +200,87 @@ work extends shared components instead of creating new local variants. must keep a route-owned canonical option visible in shared selects like `LabeledFilterSelect` even when current results do not contain that option, so provider-scoped handoffs do not flash back to `All`. -10. Keep the first welcome screen in - `frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx` - explicit about operator context. The shell must explain that the bootstrap - token only unlocks first-run setup, state where the command should run, and - adapt command/help text to detected Docker or containerized deployments - instead of assuming the operator already knows which host or container owns - the Pulse install. -11. Keep the settings-shell infrastructure landing path aligned with that same - first-session story. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` - must treat `/settings` and the infrastructure settings tab as the canonical - path to `/settings/infrastructure/install`, not to reporting/control, so - the shell does not send first-time operators to the wrong infrastructure - subview by default. -12. Keep dashboard onboarding copy on the shared presentation owner in - `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`. Both the - infrastructure empty state and the dashboard route's no-resources state - must name the canonical install workspace explicitly, keep `Platform - connections` visible as the API-backed alternative for Proxmox and - TrueNAS, and expose the same first-host next step instead of falling back - to passive “nothing here yet” wording. -13. Keep cross-surface investigation handoffs on shared route ownership. -14. Keep shared summary-card emphasis coherent. When shared summary primitives enter an `inactive` state, `SummaryMetricCard`, `InteractiveSparkline`, and `DensityMap` must all demote background context together so storage, infrastructure, and workloads read as one interaction model instead of mixing page-local opacity, sticky-shell, or highlight rules. +11. Keep the first welcome screen in + `frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx` + explicit about operator context. The shell must explain that the bootstrap + token only unlocks first-run setup, state where the command should run, and + adapt command/help text to detected Docker or containerized deployments + instead of assuming the operator already knows which host or container owns + the Pulse install. +12. Keep the settings-shell infrastructure landing path aligned with that same + first-session story. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` + must treat `/settings` and the infrastructure settings tab as the canonical + path to `/settings/infrastructure/install`, not to reporting/control, so + the shell does not send first-time operators to the wrong infrastructure + subview by default. +13. Keep dashboard onboarding copy on the shared presentation owner in + `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`. Both the + infrastructure empty state and the dashboard route's no-resources state + must name the canonical install workspace explicitly, keep `Platform +connections` visible as the API-backed alternative for Proxmox and + TrueNAS, and expose the same first-host next step instead of falling back + to passive “nothing here yet” wording. +14. Keep cross-surface investigation handoffs on shared route ownership. Feature shells such as Alerts and Patrol may decide which governed destination chips to render, but canonical href, label, dedupe, and infrastructure-fallback truth must stay in `frontend-modern/src/routing/resourceLinks.ts` instead of freezing raw route strings or provider-local link builders inside feature panels. -15. Keep shared contextual focus canonical after adoption. Once a summary or table surface enters route-backed contextual focus, future additions must extend `frontend-modern/src/components/shared/contextualFocus.ts` and its guardrail tests rather than forking another helper for workload IDs, resource IDs, or scroll-preserving same-route selection. -14. Keep shared infrastructure/resource selectors on the canonical agent-facet +15. Keep shared summary-card emphasis coherent. When shared summary primitives enter an `inactive` state, `SummaryMetricCard`, `InteractiveSparkline`, and `DensityMap` must all demote background context together so storage, infrastructure, and workloads read as one interaction model instead of mixing page-local opacity, sticky-shell, or highlight rules. +16. Keep density-map summaries overview-first. When a shared summary density map receives row focus or chart-hover emphasis, `frontend-modern/src/components/shared/DensityMap.tsx`, `frontend-modern/src/components/shared/useDensityMapState.ts`, and `frontend-modern/src/components/shared/densityMapModel.ts` must preserve the multi-entity overview rows and layer focused-entity detail inside the same card instead of swapping the card into a single-series chart or dimming the rest of the map into unusable background noise. +17. Keep shared contextual focus canonical after adoption. Once a summary or table surface enters route-backed contextual focus, future additions must extend `frontend-modern/src/components/shared/contextualFocus.ts` and its guardrail tests rather than forking another helper for workload IDs, resource IDs, or scroll-preserving same-route selection. +18. Keep shared infrastructure/resource selectors on the canonical agent-facet truth. Shared primitives and settings-facing selector helpers must treat top-level TrueNAS appliances as agent-facet infrastructure via shared helper ownership instead of reviving a direct `resource.type === 'truenas'` branch inside page shells, selectors, or reporting-resource type helpers. -15. Keep shared feature-shell Patrol run fixtures on the canonical run-record +19. Keep shared feature-shell Patrol run fixtures on the canonical run-record contract. When `frontend-modern/src/features/patrol/` consumes Patrol run history, the shared normalized record must preserve provider-backed counts such as `truenas_checked` instead of letting feature-local fixtures or fallback objects collapse API-backed TrueNAS systems back into generic agent-host presentation. -16. Keep the authenticated app root aligned with that same first-session path. -That same shared-primitive ownership now includes contextual row focus. -`frontend-modern/src/components/shared/contextualFocus.ts` is the canonical -owner for interactive-series filtering, focused-label lookup, active-series -resolution, and nearest-scrollable-ancestor preservation across page-scoped -summary surfaces. Dashboard row focus, infrastructure summary emphasis, -storage summary emphasis, and workloads summary emphasis must all route through -that helper instead of maintaining page-local copies of the same hover/focus -rules. +20. Keep the authenticated app root aligned with that same first-session path. + That same shared-primitive ownership now includes contextual row focus. + `frontend-modern/src/components/shared/contextualFocus.ts` is the canonical + owner for interactive-series filtering, focused-label lookup, active-series + resolution, and nearest-scrollable-ancestor preservation across page-scoped + summary surfaces. Dashboard row focus, infrastructure summary emphasis, + storage summary emphasis, and workloads summary emphasis must all route through + that helper instead of maintaining page-local copies of the same hover/focus + rules. `frontend-modern/src/App.tsx` must land `/` on the dashboard shell and let the governed dashboard empty state route first-time operators into Infrastructure Install, instead of preserving a separate root-only jump to `/infrastructure` that drifts from the rest of the onboarding contract. -17. Keep relay settings shell copy on the shared presentation owner in +21. Keep relay settings shell copy on the shared presentation owner in `frontend-modern/src/utils/relayPresentation.ts`. The route metadata in `settingsHeaderMeta.ts` and the leading `SettingsPanel` in `RelaySettingsPanel.tsx` must reuse the same description and availability copy instead of drifting into separate rollout or pairing wording. -15. Keep shared settings-shell legal and docs referrals on +22. Keep shared settings-shell legal and docs referrals on `frontend-modern/src/utils/docsLinks.ts`. Shared settings surfaces such as `AIRuntimeControlsSection.tsx` must not hardcode GitHub `main` doc URLs for privacy, security, proxy-auth, scope-reference, or Terms-of-Service links. -16. Keep shared settings-shell telemetry transparency controls on the governed +23. Keep shared settings-shell telemetry transparency controls on the governed general settings panel. Preview/reset affordances for anonymous telemetry must stay rendered inside `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` instead of drifting into route-local modals, hidden dev tools, or shell chrome that operators would not naturally inspect. -15. Keep the short telemetry/privacy summary copy on that same shared surface +24. Keep the short telemetry/privacy summary copy on that same shared surface accurate to the governed privacy doc. If the trust boundary depends on a specific retention window or on “IP addresses are not stored” rather than “IPs are never seen,” the summary copy in `GeneralSettingsPanel.tsx` must state those facts plainly instead of reverting to a stronger but inaccurate shorthand. -16. Keep shared storage-route feature presentation on neutral capability truth. +25. Keep shared storage-route feature presentation on neutral capability truth. Reusable mappers and presenters in `frontend-modern/src/features/storageBackups/` must distinguish inventory datastores from backup repositories so VMware rows on the shared storage route stay canonical to the admitted phase-1 floor instead of reviving backup-target, protected-target, or recovery-local semantics on a shared page. -16. Keep infrastructure settings-shell API alternatives on the shared shell +26. Keep infrastructure settings-shell API alternatives on the shared shell contract. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`, `frontend-modern/src/components/Settings/settingsNavigationModel.ts`, and @@ -287,7 +288,7 @@ rules. present `Platform connections` as the canonical API-backed alternative for Proxmox, TrueNAS, and future provider integrations instead of reviving top-level `Direct Proxmox` wording or shell-local provider routes. -17. Keep the infrastructure settings platform-connections summary and provider +27. Keep the infrastructure settings platform-connections summary and provider workspaces on one shared state source. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, @@ -295,14 +296,14 @@ rules. derive TrueNAS connection counts and availability from the shared infrastructure settings state instead of letting the reporting summary and the provider-specific panel issue separate connection fetches. -18. Keep alert-history feature composition on the current owned state contract. +28. Keep alert-history feature composition on the current owned state contract. `frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` must react to the shared `alertData()` history state instead of reviving deleted aliases, and it must pass unified-resource resolution through to `frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx` so the panel can render shared route chips without creating another page-local resource lookup or provider-specific handoff layer. -19. Keep the alert-thresholds containers surface on the canonical shared owner. +29. Keep the alert-thresholds containers surface on the canonical shared owner. `alertOverridesModel.ts`, `useAlertOverridesState.ts`, and `useAlertsConfigurationState.ts` must surface API-backed `app-container` parents such as TrueNAS as first-class `Container Runtimes`, while 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 1aa6d9e3a..e51bbd727 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -90,35 +90,35 @@ regression protection. 68. `frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx` 69. `frontend-modern/src/components/Workloads/WorkloadsSummary.tsx` 70. `frontend-modern/src/utils/throughputPresentation.ts` -69. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx` -70. `frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx` -71. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts` -72. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` -73. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` -74. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx` -75. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx` -76. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts` -77. `frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts` -78. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx` -79. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx` -80. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx` -81. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx` -82. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx` -83. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts` -84. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx` -85. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx` -86. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx` -87. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx` -88. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx` -89. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx` -90. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx` -91. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts` -92. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx` -93. `frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts` -94. `frontend-modern/src/components/Dashboard/__tests__/useDashboardWorkloadViewportSync.test.tsx` -95. `frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx` -96. `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts` -97. `frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts` +71. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx` +72. `frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx` +73. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts` +74. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` +75. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` +76. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx` +77. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx` +78. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts` +79. `frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts` +80. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx` +81. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx` +82. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx` +83. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx` +84. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx` +85. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts` +86. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx` +87. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx` +88. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx` +89. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx` +90. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx` +91. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx` +92. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx` +93. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts` +94. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx` +95. `frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts` +96. `frontend-modern/src/components/Dashboard/__tests__/useDashboardWorkloadViewportSync.test.tsx` +97. `frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx` +98. `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts` +99. `frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts` ## Shared Boundaries @@ -167,7 +167,7 @@ regression protection. 25. Extend dashboard workload table shell ownership through `frontend-modern/src/components/Dashboard/WorkloadTableHeader.tsx` and `frontend-modern/src/components/Dashboard/WorkloadPanel.tsx` rather than rebuilding sortable header markup, grouped node rows, row expansion, or guest-drawer rendering inside `frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx` 26. Keep long-range workload chart capping time-proportional across `frontend-modern/src/components/Workloads/WorkloadsSummary.tsx`, `frontend-modern/src/api/charts.ts`, and `internal/api/router.go`: when the workload hot path caps mixed-cadence history for top cards, it must bucket by time window rather than raw point index so 7-day and 30-day workload cards stay visually even without relaxing the protected payload budget. 27. Keep summary hover/focus and sticky-card behavior on shared hot paths: infrastructure, workloads, and storage summary shells must reuse one focused-series model plus `frontend-modern/src/components/shared/StickySummarySection.tsx` inside the app scroll shell instead of per-page scroll listeners or per-card hover derivations, so row scrubbing highlights all cards without multiplying render or scroll work. -28. Keep summary-card hover emphasis on one bounded rendering budget: when a summary row is active, shared sparkline and density-map primitives must promote the selected series and demote background series through the same active-series ID rather than layering a second page-local highlight pass, so zoom-range and hover scrubbing stay visually coherent without reintroducing multi-series overdraw on the hot summary cards. +28. Keep summary-card hover emphasis on one bounded rendering budget: when a summary row is active, shared sparkline and density-map primitives must promote the selected series and demote background series through the same active-series ID rather than layering a second page-local highlight pass, so zoom-range and hover scrubbing stay visually coherent without reintroducing multi-series overdraw on the hot summary cards. Density maps on that hot path must stay overview-first under focus: preserve the multi-entity heatmap rows, layer focused-entity detail inside the card, and avoid swapping transient hover into a separate single-series chart path. ## Forbidden Paths @@ -214,8 +214,11 @@ same-path route-state scheduler so opening a focused workload preserves scroll and does not look like a full page reload, and the governed infrastructure and workloads summary surfaces must keep the summary page-scoped while that focus reuses the shared highlight contract; density maps may retain page-level -context, but line-card isolation must still flow through the shared sparkline -runtime instead of a page-local focus overlay. +context, but they must now also surface focused-entity detail inside the same +card instead of dimming the rest of the map into unusable background noise or +swapping the hot path into a transient single-series chart. Line-card +isolation must still flow through the shared sparkline runtime instead of a +page-local focus overlay. That same hot-path rule now has one shared runtime boundary. Interactive-series filtering, active-series derivation, focused-label lookup, and local scroll-preserving row focus must extend diff --git a/frontend-modern/src/components/shared/DensityMap.tsx b/frontend-modern/src/components/shared/DensityMap.tsx index ee6f3a252..3c9acf62a 100644 --- a/frontend-modern/src/components/shared/DensityMap.tsx +++ b/frontend-modern/src/components/shared/DensityMap.tsx @@ -8,16 +8,85 @@ export type { DensityMapProps } from './densityMapModel'; export const DensityMap: Component = (props) => { const densityMap = useDensityMapState(props); const interactionState = () => props.interactionState ?? 'default'; + const formatDetailValue = (value: number | null) => + value === null ? 'No sample' : densityMap.formatValue(value); return (
+
+ + Top activity overview +
+ } + > + {(detail) => ( +
+
+ + + {detail().seriesName} + + + {(path) => ( + + )} + +
+
+
+ + Latest + + + {formatDetailValue(detail().currentValue)} + +
+
+ + Peak + + + {formatDetailValue(detail().peakValue)} + +
+
+
+ )} + +
+
{/* Y-axis: typically in a density map we might just show "Top Nodes" */}
diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index bffa0e3fb..f38221ce6 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -158,11 +158,13 @@ describe('shared primitive guardrails', () => { expect(generalSettingsPanelSource).toContain('variant="prominent"'); expect(generalSettingsPanelSource).not.toContain("props.themePreference() === 'light'"); expect(generalSettingsPanelSource).not.toContain("temperatureStore.unit() === 'celsius'"); - expect(generalSettingsPanelSource).not.toContain("props.pvePollingSelection() === option.value"); + expect(generalSettingsPanelSource).not.toContain( + 'props.pvePollingSelection() === option.value', + ); expect(reportingPanelSource.match(/"); + expect(reportingPanelSource).not.toContain(''); }); it('routes selectable settings cards through SelectionCardGroup', () => { @@ -217,9 +219,7 @@ describe('shared primitive guardrails', () => { expect(activeUseTrialNudgeSource).not.toContain('localStorage'); expect(activeUseTrialNudgeSource).not.toContain('setInterval'); - expect(activeUseTrialNudgeStateSource).toContain( - 'export function useActiveUseTrialNudgeState', - ); + expect(activeUseTrialNudgeStateSource).toContain('export function useActiveUseTrialNudgeState'); expect(activeUseTrialNudgeStateSource).toContain('createSignal'); expect(activeUseTrialNudgeStateSource).toContain('createMemo'); expect(activeUseTrialNudgeStateSource).toContain('window.localStorage'); @@ -243,7 +243,9 @@ describe('shared primitive guardrails', () => { }); it('keeps contextual row focus on one shared helper across summary consumers', () => { - expect(contextualFocusSource).toContain('export const preserveScrollableAncestorVerticalOffset'); + expect(contextualFocusSource).toContain( + 'export const preserveScrollableAncestorVerticalOffset', + ); expect(contextualFocusSource).toContain('export function useSummaryContextualFocusState'); expect(contextualFocusSource).toContain('chartHoveredSeriesId'); expect(summaryCardInteractionSource).toContain('chartHoveredSeriesId'); @@ -255,7 +257,9 @@ describe('shared primitive guardrails', () => { expect(infrastructureSummaryStateSource).toContain('useSummaryContextualFocusState'); expect(infrastructureSummaryStateSource).toContain('chartHoverSync'); - expect(infrastructureSummaryStateSource).not.toContain('const interactiveResourceIds = createMemo'); + expect(infrastructureSummaryStateSource).not.toContain( + 'const interactiveResourceIds = createMemo', + ); expect(storageSummarySource).toContain('useSummaryContextualFocusState'); expect(storageSummarySource).toContain('chartHoverSync'); @@ -408,14 +412,12 @@ describe('shared primitive guardrails', () => { 'rounded-md border border-blue-200 bg-blue-50 px-4 py-3', ); expect(reportingPanelSource).toContain('CalloutCard'); - expect(reportingPanelSource).not.toContain( - 'rounded-md border border-blue-200 bg-blue-50 p-6', - ); + expect(reportingPanelSource).not.toContain('rounded-md border border-blue-200 bg-blue-50 p-6'); }); it('keeps shared fleet limit banner copy on the monitored-system commercial term', () => { expect(monitoredSystemLimitWarningBannerModelSource).toContain( - "@/utils/monitoredSystemPresentation", + '@/utils/monitoredSystemPresentation', ); expect(monitoredSystemLimitWarningBannerModelSource).toContain( 'formatMonitoredSystemLimitSummary', @@ -433,9 +435,7 @@ describe('shared primitive guardrails', () => { expect(monitoredSystemLimitWarningBannerModelSource).not.toContain( 'do not count toward Unified Agents.', ); - expect(monitoredSystemLimitWarningBannerModelSource).not.toContain( - 'Install v6 Unified Agents', - ); + expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('Install v6 Unified Agents'); }); it('keeps monitored system limit warning banner on shell, runtime, and model owners', () => { @@ -496,7 +496,9 @@ describe('shared primitive guardrails', () => { expect(infrastructureSummaryTableStateSource).toContain('useWebSocket'); expect(infrastructureSummaryTableStateSource).toContain('useAlertsActivation'); - expect(infrastructureSummaryTableStateSource).toContain('export function useInfrastructureSummaryTableState'); + expect(infrastructureSummaryTableStateSource).toContain( + 'export function useInfrastructureSummaryTableState', + ); expect(infrastructureSummaryTableStateSource).toContain('createSignal'); expect(infrastructureSummaryTableModelSource).toContain('getNormalizedIdentityLookupVariants'); @@ -544,7 +546,9 @@ describe('shared primitive guardrails', () => { expect(interactiveSparklineSource).not.toContain('scheduleSparkline'); expect(interactiveSparklineSource).not.toContain('downsampleLTTB'); - expect(interactiveSparklineStateSource).toContain('export function useInteractiveSparklineState'); + expect(interactiveSparklineStateSource).toContain( + 'export function useInteractiveSparklineState', + ); expect(interactiveSparklineStateSource).toContain('activeSeriesDisplay'); expect(interactiveSparklineStateSource).toContain('shouldRenderSeries'); expect(interactiveSparklineStateSource).toContain('renderedSeriesCount'); @@ -570,6 +574,7 @@ describe('shared primitive guardrails', () => { expect(densityMapStateSource).toContain('window.addEventListener'); expect(densityMapModelSource).toContain('buildDensityMapChartData'); + expect(densityMapModelSource).toContain('buildDensityMapFocusDetail'); expect(densityMapModelSource).toContain('buildDensityMapHoveredState'); expect(densityMapModelSource).toContain('formatDensityMapHoverTime'); expect(densityMapModelSource).toContain('getDensityMapCellOpacity'); @@ -702,9 +707,7 @@ describe('shared primitive guardrails', () => { ); expect(infrastructureDetailsDrawerSource).not.toContain('createSignal'); expect(infrastructureDetailsDrawerSource).not.toContain('getInfrastructureMetadataId'); - expect(infrastructureDetailsDrawerSource).not.toContain( - 'getInfrastructureDiscoveryHostname', - ); + expect(infrastructureDetailsDrawerSource).not.toContain('getInfrastructureDiscoveryHostname'); expect(infrastructureDetailsDrawerStateSource).toContain( 'export function useInfrastructureDetailsDrawerState', @@ -721,9 +724,7 @@ describe('shared primitive guardrails', () => { 'resolveInfrastructureDetailsDrawerDiscoveryHostname', ); expect(infrastructureDetailsDrawerModelSource).toContain('getInfrastructureMetadataId'); - expect(infrastructureDetailsDrawerModelSource).toContain( - 'getInfrastructureDiscoveryHostname', - ); + expect(infrastructureDetailsDrawerModelSource).toContain('getInfrastructureDiscoveryHostname'); }); it('keeps mobile nav on shell, runtime, and model owners', () => { @@ -766,7 +767,9 @@ describe('shared primitive guardrails', () => { it('keeps search field on shell, runtime, and model owners', () => { expect(searchFieldSource).toContain('useSearchFieldState'); expect(searchFieldSource).not.toContain('let inputEl: HTMLInputElement'); - expect(searchFieldSource).not.toContain("if (props.hasTrailingControls) return 'pr-14 sm:pr-20'"); + expect(searchFieldSource).not.toContain( + "if (props.hasTrailingControls) return 'pr-14 sm:pr-20'", + ); expect(searchFieldSource).not.toContain("if (e.key === 'Escape'"); expect(searchFieldStateSource).toContain('export function useSearchFieldState'); @@ -875,7 +878,7 @@ describe('shared primitive guardrails', () => { expect(summaryMetricCardSource).toContain("density?: 'default' | 'compact'"); expect(summaryMetricCardSource).toContain("props.density === 'compact'"); - expect(summaryMetricCardSource).toContain("!p-1.5 sm:!p-2"); + expect(summaryMetricCardSource).toContain('!p-1.5 sm:!p-2'); expect(summaryMetricCardSource).not.toContain('Recovery Posture'); expect(summaryMetricCardSource).not.toContain('Freshness'); }); diff --git a/frontend-modern/src/components/shared/__tests__/DensityMap.test.tsx b/frontend-modern/src/components/shared/__tests__/DensityMap.test.tsx index eb8e9fbdf..ae896f2f3 100644 --- a/frontend-modern/src/components/shared/__tests__/DensityMap.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/DensityMap.test.tsx @@ -6,6 +6,7 @@ import densityMapStateSource from '@/components/shared/useDensityMapState.ts?raw import { DensityMap } from '@/components/shared/DensityMap'; import { buildDensityMapChartData, + buildDensityMapFocusDetail, getDensityMapExternalSeriesIndex, } from '@/components/shared/densityMapModel'; @@ -15,6 +16,8 @@ HTMLCanvasElement.prototype.getContext = vi.fn(() => ({ scale: vi.fn(), beginPath: vi.fn(), roundRect: vi.fn(), + save: vi.fn(), + restore: vi.fn(), stroke: vi.fn(), strokeRect: vi.fn(), fill: vi.fn(), @@ -38,6 +41,7 @@ describe('DensityMap', () => { expect(densityMapStateSource).toContain('export function useDensityMapState'); expect(densityMapModelSource).toContain('buildDensityMapChartData'); + expect(densityMapModelSource).toContain('buildDensityMapFocusDetail'); expect(densityMapModelSource).toContain('buildDensityMapHoveredState'); expect(densityMapModelSource).toContain('getDensityMapExternalSeriesIndex'); expect(densityMapModelSource).toContain('getDensityMapCellOpacity'); @@ -165,4 +169,88 @@ describe('DensityMap', () => { expect(chartData.series.map((entry) => entry.id)).toContain('series-24'); expect(getDensityMapExternalSeriesIndex(chartData, 'series-24')).not.toBeNull(); }); + + it('builds focused detail from the active density-map series without replacing the overview model', () => { + const now = Date.now(); + const chartData = buildDensityMapChartData({ + now, + timeRange: '1h', + highlightSeriesId: 'alpha', + series: [ + { + id: 'alpha', + name: 'Alpha', + color: '#10b981', + data: [ + { timestamp: now - 40_000, value: 12 }, + { timestamp: now - 10_000, value: 32 }, + ], + }, + { + id: 'beta', + name: 'Beta', + color: '#3b82f6', + data: [ + { timestamp: now - 30_000, value: 18 }, + { timestamp: now - 5_000, value: 24 }, + ], + }, + ], + }); + + const detail = buildDensityMapFocusDetail({ + data: chartData, + highlightSeriesId: 'alpha', + }); + + expect(chartData.series).toHaveLength(2); + expect(detail).toMatchObject({ + seriesId: 'alpha', + seriesName: 'Alpha', + currentValue: 32, + peakValue: 32, + }); + expect(detail?.sparklinePath).toContain('M'); + }); + + it('renders focused detail while keeping the density-map overview series count intact', () => { + const now = Date.now(); + const { container } = render(() => ( + + )); + + const root = container.firstElementChild; + expect(root?.getAttribute('data-summary-chart-kind')).toBe('density-map'); + expect(root?.getAttribute('data-rendered-series-count')).toBe('2'); + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.getByText('Latest')).toBeInTheDocument(); + expect(screen.getByText('Peak')).toBeInTheDocument(); + expect(screen.getAllByText('32.0')).toHaveLength(2); + const overlay = container.querySelector('[data-density-map-focus-detail="true"]'); + expect(overlay).not.toBeNull(); + expect(overlay).toHaveAttribute('data-density-map-focus-series-id', 'alpha'); + }); }); diff --git a/frontend-modern/src/components/shared/densityMapModel.ts b/frontend-modern/src/components/shared/densityMapModel.ts index 97be16a83..59d6c4330 100644 --- a/frontend-modern/src/components/shared/densityMapModel.ts +++ b/frontend-modern/src/components/shared/densityMapModel.ts @@ -27,6 +27,18 @@ export interface DensityMapHoveredState { seriesIndex: number; } +export interface DensityMapFocusDetail { + currentValue: number | null; + currentTimestamp: number | null; + hoveredValue: number | null; + hoveredTimestamp: number | null; + peakValue: number | null; + seriesColor: string; + seriesId: string; + seriesName: string; + sparklinePath: string | null; +} + export interface DensityMapChartData { series: InteractiveSparklineSeries[]; grid: number[][]; @@ -224,3 +236,84 @@ export function buildDensityMapSynchronizedHoveredState(options: { seriesIndex, }; } + +export function buildDensityMapFocusDetail(options: { + activeHoveredState?: DensityMapHoveredState | null; + data: DensityMapChartData; + highlightSeriesId?: string | null; +}): DensityMapFocusDetail | null { + const activeHoveredState = options.activeHoveredState ?? null; + const seriesIndex = + activeHoveredState?.seriesIndex ?? + getDensityMapExternalSeriesIndex(options.data, options.highlightSeriesId); + if (seriesIndex === null || seriesIndex < 0 || seriesIndex >= options.data.series.length) { + return null; + } + + const series = options.data.series[seriesIndex]; + const seriesId = series.id?.trim() || ''; + if (!seriesId) { + return null; + } + + const windowEnd = options.data.windowStart + options.data.rangeMs; + const points = series.data.filter( + (point) => point.timestamp >= options.data.windowStart && point.timestamp <= windowEnd, + ); + const currentPoint = points.length > 0 ? points[points.length - 1] : null; + let peakValue: number | null = null; + for (const point of points) { + peakValue = peakValue === null ? point.value : Math.max(peakValue, point.value); + } + + const sparklinePath = buildDensityMapFocusSparklinePath({ + points, + rangeMs: options.data.rangeMs, + windowStart: options.data.windowStart, + }); + + return { + currentValue: currentPoint?.value ?? null, + currentTimestamp: currentPoint?.timestamp ?? null, + hoveredValue: activeHoveredState?.value ?? null, + hoveredTimestamp: activeHoveredState?.timestamp ?? null, + peakValue, + seriesColor: series.color, + seriesId, + seriesName: series.name || 'Unknown', + sparklinePath, + }; +} + +const buildDensityMapFocusSparklinePath = (options: { + points: Array<{ timestamp: number; value: number }>; + rangeMs: number; + windowStart: number; +}): string | null => { + const { points, rangeMs, windowStart } = options; + if (points.length < 2 || rangeMs <= 0) { + return null; + } + + const width = 64; + const height = 22; + let maxValue = 0; + for (const point of points) { + if (point.value > maxValue) { + maxValue = point.value; + } + } + if (maxValue <= 0) { + return null; + } + + const commands: string[] = []; + for (let index = 0; index < points.length; index += 1) { + const point = points[index]; + const x = clampDensityMapValue((point.timestamp - windowStart) / rangeMs, 0, 1) * width; + const y = height - clampDensityMapValue(point.value / maxValue, 0, 1) * height; + commands.push(`${index === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`); + } + + return commands.join(' '); +}; diff --git a/frontend-modern/src/components/shared/useDensityMapState.ts b/frontend-modern/src/components/shared/useDensityMapState.ts index 7befd2251..a1f6fa124 100644 --- a/frontend-modern/src/components/shared/useDensityMapState.ts +++ b/frontend-modern/src/components/shared/useDensityMapState.ts @@ -1,6 +1,7 @@ import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'; import { buildDensityMapChartData, + buildDensityMapFocusDetail, getDensityMapExternalSeriesIndex, buildDensityMapHoveredState, buildDensityMapSynchronizedHoveredState, @@ -43,6 +44,13 @@ export function useDensityMapState(props: DensityMapProps) { const activeHoveredState = createMemo(() => { return hoveredState() ?? synchronizedHoveredState(); }); + const focusDetail = createMemo(() => + buildDensityMapFocusDetail({ + activeHoveredState: activeHoveredState(), + data: chartData(), + highlightSeriesId: props.highlightSeriesId, + }), + ); const formatValue = (value: number) => formatDensityMapValue(value, props.formatValue); @@ -78,17 +86,21 @@ export function useDensityMapState(props: DensityMapProps) { activeSeriesIndex !== null && activeSeriesIndex >= 0 && activeSeriesIndex < data.series.length ? data.series[activeSeriesIndex] : null; + const hasActiveSeries = activeSeriesIndex !== null; for (let row = 0; row < rows; row += 1) { const cellY = row * cellHeight; - const isDimmed = activeSeriesIndex !== null && row !== activeSeriesIndex; + const isActiveRow = activeSeriesIndex === row; + const rowOpacity = !hasActiveSeries ? 1 : isActiveRow ? 1 : 0.42; for (let column = 0; column < DENSITY_MAP_COLUMNS; column += 1) { const cellX = column * cellWidth; const value = data.grid[row][column]; if (value <= 0) { - context.globalAlpha = (isDimmed ? 0.08 : 1) * interactionOpacity; - context.fillStyle = 'rgba(128, 128, 128, 0.05)'; + context.globalAlpha = rowOpacity * interactionOpacity; + context.fillStyle = isActiveRow + ? 'rgba(148, 163, 184, 0.08)' + : 'rgba(148, 163, 184, 0.04)'; context.fillRect( cellX + DENSITY_MAP_PADDING_X / 2, cellY + DENSITY_MAP_PADDING_Y / 2, @@ -99,9 +111,7 @@ export function useDensityMapState(props: DensityMapProps) { } context.globalAlpha = - getDensityMapCellOpacity(value, data.globalMax) * - (isDimmed ? 0.05 : 1) * - interactionOpacity; + getDensityMapCellOpacity(value, data.globalMax) * rowOpacity * interactionOpacity; context.fillStyle = data.series[row].color; if (context.roundRect) { context.beginPath(); @@ -141,10 +151,10 @@ export function useDensityMapState(props: DensityMapProps) { context.lineWidth = 1.25; if (context.roundRect) { context.beginPath(); - context.roundRect(0.5, highlightY+0.5, width-1, Math.max(cellHeight-1, 1), 4); + context.roundRect(0.5, highlightY + 0.5, width - 1, Math.max(cellHeight - 1, 1), 4); context.stroke(); } else { - context.strokeRect(0.5, highlightY+0.5, width-1, Math.max(cellHeight-1, 1)); + context.strokeRect(0.5, highlightY + 0.5, width - 1, Math.max(cellHeight - 1, 1)); } context.restore(); } @@ -201,6 +211,7 @@ export function useDensityMapState(props: DensityMapProps) { activeHoveredState, chartData, externalSeriesIndex, + focusDetail, formatValue, handleMouseLeave, handleMouseMove, diff --git a/tests/integration/tests/48-summary-hover-selection.spec.ts b/tests/integration/tests/48-summary-hover-selection.spec.ts index e2a94276f..4f354432a 100644 --- a/tests/integration/tests/48-summary-hover-selection.spec.ts +++ b/tests/integration/tests/48-summary-hover-selection.spec.ts @@ -172,7 +172,9 @@ async function hoverSummaryChartUntilActiveId( (node) => node.getAttribute("data-highlight-series-active") === "true", ) - .map((node) => node.getAttribute("data-highlight-series-id") || "") + .map( + (node) => node.getAttribute("data-highlight-series-id") || "", + ) .filter(Boolean), ), ), @@ -205,6 +207,47 @@ async function expectActiveIsolatedLineCards( .toBe(expectedCount); } +async function expectActiveDensityMapsPreserveOverview( + summary: import("@playwright/test").Locator, + resourceId: string, + expectedCount: number, +): Promise { + await expect + .poll(async () => + summary + .locator('[data-summary-chart-kind="density-map"]') + .evaluateAll((nodes, expectedId) => { + const activeNodes = nodes.filter( + (node) => + node.getAttribute("data-highlight-series-id") === expectedId && + node.getAttribute("data-highlight-series-active") === "true", + ); + return { + activeCount: activeNodes.length, + focusDetailCount: activeNodes.filter((node) => { + const detail = node.querySelector( + '[data-density-map-focus-detail="true"]', + ); + return ( + detail?.getAttribute("data-density-map-focus-series-id") === + expectedId + ); + }).length, + preservedOverview: activeNodes.every( + (node) => + Number(node.getAttribute("data-rendered-series-count") || "0") > + 1, + ), + }; + }, resourceId), + ) + .toEqual({ + activeCount: expectedCount, + focusDetailCount: expectedCount, + preservedOverview: true, + }); +} + async function readSummarySeriesId( row: import("@playwright/test").Locator, fallbackAttribute: string, @@ -265,16 +308,14 @@ test.describe.serial("Summary hover selection", () => { ); const firstInfrastructureRow = infrastructureRows.first(); await expect(firstInfrastructureRow).toBeVisible(); - const { - index: infrastructureRowIndex, - resourceId: infrastructureRowId, - } = await hoverRowUntilSummaryHighlights( - page, - infrastructureRows, - infrastructureSummary, - "data-row-id", - 4, - ); + const { index: infrastructureRowIndex, resourceId: infrastructureRowId } = + await hoverRowUntilSummaryHighlights( + page, + infrastructureRows, + infrastructureSummary, + "data-row-id", + 4, + ); const infrastructureRow = infrastructureRows.nth(infrastructureRowIndex); expect(infrastructureRowId).not.toBe(""); await expectActiveIsolatedLineCards(infrastructureSummary, 2); @@ -286,6 +327,11 @@ test.describe.serial("Summary hover selection", () => { 4, ); await expectActiveIsolatedLineCards(infrastructureSummary, 2); + await expectActiveDensityMapsPreserveOverview( + infrastructureSummary, + infrastructureRowId, + 2, + ); await page.goto("/workloads", { waitUntil: "domcontentloaded" }); const workloadsSummary = page.getByTestId("workloads-summary"); @@ -302,6 +348,11 @@ test.describe.serial("Summary hover selection", () => { ); expect(workloadRowId).not.toBe(""); await expectActiveIsolatedLineCards(workloadsSummary, 2); + await expectActiveDensityMapsPreserveOverview( + workloadsSummary, + workloadRowId, + 2, + ); const vmwareWorkloadRow = page .locator('tr[data-guest-id^="vm-"]', { hasText: "warehouse-api-01" }) @@ -315,6 +366,11 @@ test.describe.serial("Summary hover selection", () => { await dispatchRowHover(vmwareWorkloadRow); await expectSummaryHighlightCount(workloadsSummary, vmwareWorkloadId, 4); await expectActiveIsolatedLineCards(workloadsSummary, 2); + await expectActiveDensityMapsPreserveOverview( + workloadsSummary, + vmwareWorkloadId, + 2, + ); await page.goto("/storage", { waitUntil: "domcontentloaded" }); const storageSummary = page.getByTestId("storage-summary"); @@ -330,13 +386,14 @@ test.describe.serial("Summary hover selection", () => { const storagePoolRows = page.locator("tr[data-row-id]"); const firstStoragePoolRow = storagePoolRows.first(); await expect(firstStoragePoolRow).toBeVisible(); - const { resourceId: storagePoolRowId } = await hoverRowUntilSummaryHighlights( - page, - storagePoolRows, - storageSummary, - "data-row-id", - 3, - ); + const { resourceId: storagePoolRowId } = + await hoverRowUntilSummaryHighlights( + page, + storagePoolRows, + storageSummary, + "data-row-id", + 3, + ); expect(storagePoolRowId).not.toBe(""); await expectActiveIsolatedLineCards(storageSummary, 3); @@ -344,13 +401,14 @@ test.describe.serial("Summary hover selection", () => { const storageDiskRows = page.locator("tr[data-row-id]"); const firstStorageDiskRow = storageDiskRows.first(); await expect(firstStorageDiskRow).toBeVisible(); - const { resourceId: storageDiskRowId } = await hoverRowUntilSummaryHighlights( - page, - storageDiskRows, - storageSummary, - "data-row-id", - 1, - ); + const { resourceId: storageDiskRowId } = + await hoverRowUntilSummaryHighlights( + page, + storageDiskRows, + storageSummary, + "data-row-id", + 1, + ); expect(storageDiskRowId).not.toBe(""); await expectActiveIsolatedLineCards(storageSummary, 1); @@ -377,7 +435,9 @@ test.describe.serial("Summary hover selection", () => { const infrastructureChartId = await hoverSummaryChartUntilActiveId( page, infrastructureSummary, - infrastructureSummary.locator('[data-active-series-display="isolate"]').first(), + infrastructureSummary + .locator('[data-active-series-display="isolate"]') + .first(), ); expect(infrastructureChartId).not.toBe(""); await expectSummaryHighlightCount( @@ -386,6 +446,11 @@ test.describe.serial("Summary hover selection", () => { 4, ); await expectActiveIsolatedLineCards(infrastructureSummary, 2); + await expectActiveDensityMapsPreserveOverview( + infrastructureSummary, + infrastructureChartId, + 2, + ); await page.goto("/workloads", { waitUntil: "domcontentloaded" }); const workloadsSummary = page.getByTestId("workloads-summary"); @@ -393,11 +458,18 @@ test.describe.serial("Summary hover selection", () => { const workloadChartId = await hoverSummaryChartUntilActiveId( page, workloadsSummary, - workloadsSummary.locator('[data-active-series-display="isolate"]').first(), + workloadsSummary + .locator('[data-active-series-display="isolate"]') + .first(), ); expect(workloadChartId).not.toBe(""); await expectSummaryHighlightCount(workloadsSummary, workloadChartId, 4); await expectActiveIsolatedLineCards(workloadsSummary, 2); + await expectActiveDensityMapsPreserveOverview( + workloadsSummary, + workloadChartId, + 2, + ); await page.goto("/storage", { waitUntil: "domcontentloaded" }); const storageSummary = page.getByTestId("storage-summary");