mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 23:36:37 +00:00
fix(frontend): integrate density-map focus detail
This commit is contained in:
parent
8a44d27675
commit
1339e17eaa
8 changed files with 558 additions and 218 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,16 +8,85 @@ export type { DensityMapProps } from './densityMapModel';
|
|||
export const DensityMap: Component<DensityMapProps> = (props) => {
|
||||
const densityMap = useDensityMapState(props);
|
||||
const interactionState = () => props.interactionState ?? 'default';
|
||||
const formatDetailValue = (value: number | null) =>
|
||||
value === null ? 'No sample' : densityMap.formatValue(value);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`relative w-full h-full flex flex-col group transition-opacity duration-150 ease-out ${
|
||||
interactionState() === 'inactive' ? 'opacity-35' : 'opacity-100'
|
||||
}`.trim()}
|
||||
data-summary-chart-kind="density-map"
|
||||
data-highlight-series-id={props.highlightSeriesId ?? ''}
|
||||
data-highlight-series-active={densityMap.externalSeriesIndex() !== null ? 'true' : 'false'}
|
||||
data-rendered-series-count={densityMap.chartData().series.length}
|
||||
data-summary-chart-state={interactionState()}
|
||||
>
|
||||
<div class="mx-1 mb-1 flex h-7 items-center overflow-hidden">
|
||||
<Show
|
||||
when={densityMap.focusDetail()}
|
||||
fallback={
|
||||
<div class="w-full truncate text-[9px] font-medium uppercase tracking-[0.12em] text-slate-500/80">
|
||||
Top activity overview
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(detail) => (
|
||||
<div
|
||||
class="flex w-full min-w-0 items-center gap-2 text-[10px]"
|
||||
data-density-map-focus-detail="true"
|
||||
data-density-map-focus-series-id={detail().seriesId}
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<span
|
||||
class="h-2 w-2 shrink-0 rounded-full"
|
||||
style={{ background: detail().seriesColor }}
|
||||
/>
|
||||
<span class="truncate font-semibold uppercase tracking-[0.1em] text-slate-600 dark:text-slate-300">
|
||||
{detail().seriesName}
|
||||
</span>
|
||||
<Show when={detail().sparklinePath}>
|
||||
{(path) => (
|
||||
<svg
|
||||
viewBox="0 0 64 22"
|
||||
class="h-4 w-12 shrink-0 overflow-visible text-slate-400"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d={path()}
|
||||
fill="none"
|
||||
stroke={detail().seriesColor}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.75"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-3 leading-none">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-[9px] uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
Latest
|
||||
</span>
|
||||
<span class="mt-0.5 font-semibold text-slate-900 dark:text-slate-50">
|
||||
{formatDetailValue(detail().currentValue)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-[9px] uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
Peak
|
||||
</span>
|
||||
<span class="mt-0.5 font-semibold text-slate-900 dark:text-slate-50">
|
||||
{formatDetailValue(detail().peakValue)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 relative min-h-0 bg-transparent flex">
|
||||
{/* Y-axis: typically in a density map we might just show "Top Nodes" */}
|
||||
<div class="absolute left-0 top-0 h-full w-full pointer-events-none flex flex-col justify-between py-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
|
|
|
|||
|
|
@ -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(/<FilterButtonGroup/g) ?? []).toHaveLength(2);
|
||||
expect(reportingPanelSource).toContain('variant="prominent"');
|
||||
expect(reportingPanelSource).not.toContain('getReportingToggleButtonClass');
|
||||
expect(reportingPanelSource).not.toContain("<For each={REPORTING_RANGE_OPTIONS}>");
|
||||
expect(reportingPanelSource).not.toContain('<For each={REPORTING_RANGE_OPTIONS}>');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => (
|
||||
<DensityMap
|
||||
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 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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(' ');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<DensityMapHoveredState | null>(() => {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue