From 70d34324fe1e608db91ea2af876067b7414f86e2 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 23 Mar 2026 02:20:31 +0000 Subject: [PATCH] Split history chart render owners --- .../subsystems/frontend-primitives.md | 5 + .../src/components/shared/HistoryChart.tsx | 180 +----------------- .../components/shared/HistoryChartHeader.tsx | 75 ++++++++ .../components/shared/HistoryChartOverlay.tsx | 100 ++++++++++ .../components/shared/HistoryChartTooltip.tsx | 36 ++++ .../SharedPrimitives.guardrails.test.ts | 23 +++ .../shared/__tests__/HistoryChart.test.tsx | 23 +++ .../components/shared/useHistoryChartState.ts | 3 +- .../frontendResourceTypeBoundaries.test.ts | 14 ++ 9 files changed, 286 insertions(+), 173 deletions(-) create mode 100644 frontend-modern/src/components/shared/HistoryChartHeader.tsx create mode 100644 frontend-modern/src/components/shared/HistoryChartOverlay.tsx create mode 100644 frontend-modern/src/components/shared/HistoryChartTooltip.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 98f9133cf..8801cb8e9 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -222,6 +222,11 @@ and hover state, and `frontend-modern/src/components/shared/historyChartModel.ts owns tooltip formatting, scale and axis math, and closest-point selection. Future history-chart work should extend those owners instead of pushing fetch, license, or canvas math back into the shared component shell. +The remaining header, overlay, and tooltip render surfaces now live in +`frontend-modern/src/components/shared/HistoryChartHeader.tsx`, +`frontend-modern/src/components/shared/HistoryChartOverlay.tsx`, and +`frontend-modern/src/components/shared/HistoryChartTooltip.tsx` instead of +re-accumulating those sections inline in the shell. The shared container update badge now follows that same owner split. `frontend-modern/src/components/shared/ContainerUpdateBadge.tsx` stays the render surface for the badge, icon, and update button shells, diff --git a/frontend-modern/src/components/shared/HistoryChart.tsx b/frontend-modern/src/components/shared/HistoryChart.tsx index c886a8080..b7fdd0b23 100644 --- a/frontend-modern/src/components/shared/HistoryChart.tsx +++ b/frontend-modern/src/components/shared/HistoryChart.tsx @@ -1,6 +1,8 @@ -import { Component, Show } from 'solid-js'; -import { Portal } from 'solid-js/web'; -import { formatHistoryChartTooltipValue, type HistoryChartProps } from './historyChartModel'; +import { Component } from 'solid-js'; +import type { HistoryChartProps } from './historyChartModel'; +import { HistoryChartHeader } from './HistoryChartHeader'; +import { HistoryChartOverlay } from './HistoryChartOverlay'; +import { HistoryChartTooltip } from './HistoryChartTooltip'; import { useHistoryChartState } from './useHistoryChartState'; export type { HistoryChartProps } from './historyChartModel'; @@ -18,65 +20,7 @@ export const HistoryChart: Component = (props) => {
-
-
- {props.label || 'History'} - - ({props.unit}) - - - - {chart.source() === 'live' ? 'Live' : 'Buffer'} - - -
- -
- -
- - Min - - {formatHistoryChartTooltipValue(chart.dataMin()!, props.unit)} - - - - Max - - {formatHistoryChartTooltipValue(chart.dataMax()!, props.unit)} - - -
-
- -
- {chart.ranges.map((range) => ( - - ))} -
-
-
-
+
= (props) => { onMouseMove={chart.handleMouseMove} onMouseLeave={chart.handleMouseLeave} /> - - -
-
-
- - - - - - - -
-

Collecting data... History will appear here.

-
-
-
- - -
-
-
- - - -
-

{chart.error()}

-
-
- - -
-
- - - - -
-

{chart.lockDays()}-Day History

-

- Upgrade to {chart.lockTierLabel()} to unlock {chart.lockDays()} days of historical - data retention. -

- -
-
+
- - - {(point) => ( -
-
- {new Date(point().timestamp).toLocaleString()} -
-
- {formatHistoryChartTooltipValue(point().value, props.unit)} -
-
- )} -
-
+
); }; diff --git a/frontend-modern/src/components/shared/HistoryChartHeader.tsx b/frontend-modern/src/components/shared/HistoryChartHeader.tsx new file mode 100644 index 000000000..a5dcb4540 --- /dev/null +++ b/frontend-modern/src/components/shared/HistoryChartHeader.tsx @@ -0,0 +1,75 @@ +import { Component, Show } from 'solid-js'; +import { formatHistoryChartTooltipValue } from './historyChartModel'; +import type { HistoryChartState } from './useHistoryChartState'; + +interface HistoryChartHeaderProps { + chart: HistoryChartState; + compact?: boolean; + hideSelector?: boolean; + label?: string; + unit?: string; +} + +export const HistoryChartHeader: Component = (props) => { + return ( +
+
+ {props.label || 'History'} + + ({props.unit}) + + + + {props.chart.source() === 'live' ? 'Live' : 'Buffer'} + + +
+ +
+ +
+ + Min + + {formatHistoryChartTooltipValue(props.chart.dataMin()!, props.unit)} + + + + Max + + {formatHistoryChartTooltipValue(props.chart.dataMax()!, props.unit)} + + +
+
+ +
+ {props.chart.ranges.map((range) => ( + + ))} +
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/shared/HistoryChartOverlay.tsx b/frontend-modern/src/components/shared/HistoryChartOverlay.tsx new file mode 100644 index 000000000..3c4fa0128 --- /dev/null +++ b/frontend-modern/src/components/shared/HistoryChartOverlay.tsx @@ -0,0 +1,100 @@ +import { Component, Show } from 'solid-js'; +import type { HistoryChartState } from './useHistoryChartState'; + +interface HistoryChartOverlayProps { + chart: HistoryChartState; + hideLock?: boolean; +} + +export const HistoryChartOverlay: Component = (props) => { + return ( + <> + +
+
+
+ + + + + + + +
+

Collecting data... History will appear here.

+
+
+
+ + +
+
+
+ + + +
+

{props.chart.error()}

+
+
+ + +
+
+ + + + +
+

{props.chart.lockDays()}-Day History

+

+ Upgrade to {props.chart.lockTierLabel()} to unlock {props.chart.lockDays()} days of + historical data retention. +

+ +
+
+ + ); +}; diff --git a/frontend-modern/src/components/shared/HistoryChartTooltip.tsx b/frontend-modern/src/components/shared/HistoryChartTooltip.tsx new file mode 100644 index 000000000..063777b26 --- /dev/null +++ b/frontend-modern/src/components/shared/HistoryChartTooltip.tsx @@ -0,0 +1,36 @@ +import { Component, Show } from 'solid-js'; +import { Portal } from 'solid-js/web'; +import { formatHistoryChartTooltipValue, type HistoryChartHoverPoint } from './historyChartModel'; + +interface HistoryChartTooltipProps { + hoveredPoint: HistoryChartHoverPoint | null; + unit?: string; +} + +export const HistoryChartTooltip: Component = (props) => { + return ( + + + {(point) => ( +
+
+ {new Date(point().timestamp).toLocaleString()} +
+
+ {formatHistoryChartTooltipValue(point().value, props.unit)} +
+
+ )} +
+
+ ); +}; diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index b0970bc27..152968460 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -9,8 +9,11 @@ import densityMapModelSource from '@/components/shared/densityMapModel.ts?raw'; import filterButtonGroupSource from '@/components/shared/FilterButtonGroup.tsx?raw'; import helpIconSource from '@/components/shared/HelpIcon.tsx?raw'; import helpIconModelSource from '@/components/shared/helpIconModel.ts?raw'; +import historyChartHeaderSource from '@/components/shared/HistoryChartHeader.tsx?raw'; +import historyChartOverlaySource from '@/components/shared/HistoryChartOverlay.tsx?raw'; import historyChartSource from '@/components/shared/HistoryChart.tsx?raw'; import historyChartModelSource from '@/components/shared/historyChartModel.ts?raw'; +import historyChartTooltipSource from '@/components/shared/HistoryChartTooltip.tsx?raw'; import infrastructureDetailsDrawerSource from '@/components/shared/InfrastructureDetailsDrawer.tsx?raw'; import infrastructureDetailsDrawerModelSource from '@/components/shared/infrastructureDetailsDrawerModel.ts?raw'; import mobileNavBarSource from '@/components/shared/MobileNavBar.tsx?raw'; @@ -247,19 +250,39 @@ describe('shared primitive guardrails', () => { it('keeps history chart on shell, runtime, and model owners', () => { expect(historyChartSource).toContain('useHistoryChartState'); + expect(historyChartSource).toContain('HistoryChartHeader'); + expect(historyChartSource).toContain('HistoryChartOverlay'); + expect(historyChartSource).toContain('HistoryChartTooltip'); expect(historyChartSource).not.toContain('ChartsAPI.getMetricsHistory'); expect(historyChartSource).not.toContain('calculateOptimalPoints'); expect(historyChartSource).not.toContain('setupCanvasDPR'); expect(historyChartSource).not.toContain('createSignal'); + expect(historyChartSource).not.toContain('Collecting data... History will appear here.'); + expect(historyChartSource).not.toContain('Unlock {chart.lockTierLabel()} Features'); expect(historyChartStateSource).toContain('ChartsAPI.getMetricsHistory'); expect(historyChartStateSource).toContain('calculateOptimalPoints'); expect(historyChartStateSource).toContain('setupCanvasDPR'); expect(historyChartStateSource).toContain('export function useHistoryChartState'); + expect(historyChartStateSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartModelSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('getHistoryChartScale'); expect(historyChartModelSource).toContain('findHistoryChartClosestPoint'); + + expect(historyChartHeaderSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartHeaderSource).not.toContain('ChartsAPI.getMetricsHistory'); + expect(historyChartHeaderSource).not.toContain('setupCanvasDPR'); + + expect(historyChartOverlaySource).toContain('Collecting data... History will appear here.'); + expect(historyChartOverlaySource).toContain('Unlock {props.chart.lockTierLabel()} Features'); + expect(historyChartOverlaySource).not.toContain('ChartsAPI.getMetricsHistory'); + expect(historyChartOverlaySource).not.toContain('setupCanvasDPR'); + + expect(historyChartTooltipSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartTooltipSource).toContain('new Date(point().timestamp).toLocaleString()'); + expect(historyChartTooltipSource).not.toContain('ChartsAPI.getMetricsHistory'); }); it('keeps container update badge on shell, runtime, and model owners', () => { diff --git a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx index bdf10732b..bc01b5c55 100644 --- a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx @@ -1,8 +1,11 @@ import { describe, expect, it, vi } from 'vitest'; import { render, screen } from '@solidjs/testing-library'; +import historyChartHeaderSource from '@/components/shared/HistoryChartHeader.tsx?raw'; +import historyChartOverlaySource from '@/components/shared/HistoryChartOverlay.tsx?raw'; import historyChartSource from '@/components/shared/HistoryChart.tsx?raw'; import historyChartModelSource from '@/components/shared/historyChartModel.ts?raw'; import historyChartStateSource from '@/components/shared/useHistoryChartState.ts?raw'; +import historyChartTooltipSource from '@/components/shared/HistoryChartTooltip.tsx?raw'; import { HistoryChart } from '@/components/shared/HistoryChart'; if (typeof globalThis.ResizeObserver === 'undefined') { @@ -50,19 +53,39 @@ vi.mock('@/api/charts', () => ({ describe('HistoryChart', () => { it('keeps the history chart on shell, runtime, and model owners', () => { expect(historyChartSource).toContain('useHistoryChartState'); + expect(historyChartSource).toContain('HistoryChartHeader'); + expect(historyChartSource).toContain('HistoryChartOverlay'); + expect(historyChartSource).toContain('HistoryChartTooltip'); expect(historyChartSource).not.toContain('ChartsAPI.getMetricsHistory'); expect(historyChartSource).not.toContain('calculateOptimalPoints'); expect(historyChartSource).not.toContain('setupCanvasDPR'); expect(historyChartSource).not.toContain('createSignal'); + expect(historyChartSource).not.toContain('Collecting data... History will appear here.'); + expect(historyChartSource).not.toContain('Unlock {chart.lockTierLabel()} Features'); expect(historyChartStateSource).toContain('ChartsAPI.getMetricsHistory'); expect(historyChartStateSource).toContain('calculateOptimalPoints'); expect(historyChartStateSource).toContain('setupCanvasDPR'); expect(historyChartStateSource).toContain('export function useHistoryChartState'); + expect(historyChartStateSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartModelSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('getHistoryChartScale'); expect(historyChartModelSource).toContain('findHistoryChartClosestPoint'); + + expect(historyChartHeaderSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartHeaderSource).not.toContain('ChartsAPI.getMetricsHistory'); + expect(historyChartHeaderSource).not.toContain('setupCanvasDPR'); + + expect(historyChartOverlaySource).toContain('Collecting data... History will appear here.'); + expect(historyChartOverlaySource).toContain('Unlock {props.chart.lockTierLabel()} Features'); + expect(historyChartOverlaySource).not.toContain('ChartsAPI.getMetricsHistory'); + expect(historyChartOverlaySource).not.toContain('setupCanvasDPR'); + + expect(historyChartTooltipSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartTooltipSource).toContain('new Date(point().timestamp).toLocaleString()'); + expect(historyChartTooltipSource).not.toContain('ChartsAPI.getMetricsHistory'); }); it('renders the default history label', () => { diff --git a/frontend-modern/src/components/shared/useHistoryChartState.ts b/frontend-modern/src/components/shared/useHistoryChartState.ts index 79eddeb68..59eb727fa 100644 --- a/frontend-modern/src/components/shared/useHistoryChartState.ts +++ b/frontend-modern/src/components/shared/useHistoryChartState.ts @@ -25,6 +25,7 @@ import { getTrialTryAgainLaterMessage, } from '@/utils/upgradePresentation'; import { + HISTORY_CHART_RANGES, createHistoryChartGeometry, findHistoryChartClosestPoint, formatHistoryChartTimeLabel, @@ -417,7 +418,7 @@ export function useHistoryChartState(props: HistoryChartProps, refs: HistoryChar lockDays, lockTierLabel, range, - ranges: ['24h', '7d', '30d', '90d'] as HistoryTimeRange[], + ranges: HISTORY_CHART_RANGES, source, startingTrial, trackUpgradeClicked, diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index cacb3e54f..53de8fd4e 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -16,8 +16,11 @@ import densityMapSource from '@/components/shared/DensityMap.tsx?raw'; import densityMapModelSource from '@/components/shared/densityMapModel.ts?raw'; import helpIconSource from '@/components/shared/HelpIcon.tsx?raw'; import helpIconModelSource from '@/components/shared/helpIconModel.ts?raw'; +import historyChartHeaderSource from '@/components/shared/HistoryChartHeader.tsx?raw'; +import historyChartOverlaySource from '@/components/shared/HistoryChartOverlay.tsx?raw'; import historyChartSource from '@/components/shared/HistoryChart.tsx?raw'; import historyChartModelSource from '@/components/shared/historyChartModel.ts?raw'; +import historyChartTooltipSource from '@/components/shared/HistoryChartTooltip.tsx?raw'; import mobileNavBarSource from '@/components/shared/MobileNavBar.tsx?raw'; import mobileNavBarModelSource from '@/components/shared/mobileNavBarModel.ts?raw'; import infrastructureSelectorSource from '@/components/shared/InfrastructureSelector.tsx?raw'; @@ -2713,14 +2716,25 @@ describe('frontend resource type boundaries', () => { expect(densityMapModelSource).toContain('formatDensityMapHoverTime'); expect(densityMapModelSource).toContain('getDensityMapCellOpacity'); expect(historyChartSource).toContain('useHistoryChartState'); + expect(historyChartSource).toContain('HistoryChartHeader'); + expect(historyChartSource).toContain('HistoryChartOverlay'); + expect(historyChartSource).toContain('HistoryChartTooltip'); expect(historyChartSource).not.toContain('ChartsAPI.getMetricsHistory'); expect(historyChartSource).not.toContain('calculateOptimalPoints'); expect(historyChartSource).not.toContain('setupCanvasDPR'); + expect(historyChartSource).not.toContain('Collecting data... History will appear here.'); + expect(historyChartSource).not.toContain('Unlock {chart.lockTierLabel()} Features'); expect(historyChartStateSource).toContain('ChartsAPI.getMetricsHistory'); expect(historyChartStateSource).toContain('calculateOptimalPoints'); expect(historyChartStateSource).toContain('setupCanvasDPR'); + expect(historyChartStateSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartModelSource).toContain('HISTORY_CHART_RANGES'); expect(historyChartModelSource).toContain('getHistoryChartScale'); + expect(historyChartHeaderSource).toContain('formatHistoryChartTooltipValue'); + expect(historyChartOverlaySource).toContain('Collecting data... History will appear here.'); + expect(historyChartOverlaySource).toContain('Unlock {props.chart.lockTierLabel()} Features'); + expect(historyChartTooltipSource).toContain('formatHistoryChartTooltipValue'); expect(containerUpdateBadgeSource).toContain('useContainerUpdateButtonState'); expect(containerUpdateBadgeSource).toContain('getUpdateButtonClass'); expect(containerUpdateBadgeSource).not.toContain('MonitoringAPI.updateDockerContainer');