From 27debb53fe703de7efb3392d565ae97cdcfd93c5 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 13 Apr 2026 15:29:43 -0300 Subject: [PATCH 1/5] refactor: Migrate org-usage charts to , drop legacy recharts SCSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the two raw-recharts consumers with the shared component introduced in #7215. Both pages keep their behaviour and visual contract; the JSX collapses substantially. BarChart API additions to support these consumers: - `barSize?: number` — fixed bar width in pixels (OrganisationUsage uses 14px to fit four series per day comfortably) - `verticalGrid?: boolean` (default true) — toggles CartesianGrid's vertical lines (legacy charts hide them) Migrations: - OrganisationUsage.container.tsx (4 metric series stacked, custom display labels via seriesLabels, selection-driven series filter, pre-formatted day axis) - SingleSDKLabelsChart.tsx (per-SDK stacked, palette colour map passed in from parent, MultiSelect-driven SDK filter) - OrganisationUsageMetrics.container.tsx — pre-formats day to 'D MMM' (matches the new chart-side day-as-display-string contract), switches userAgentColorMap from Map to Record (consistent with BarChart's prop after #7215) Cleanup: - Delete web/styles/3rdParty/_recharts.scss entirely — its rules existed solely to style the two legacy charts' tooltips. With both consumers migrated, no consumer of recharts global classNames remains; the new BarChart's ChartTooltip uses Bootstrap utilities + semantic token classes directly. Drop the @import too. - SingleSDKLabelsChart loses unused props (`metricKey`, `colours`) that the migration made redundant. Co-Authored-By: Claude Opus 4.6 --- frontend/web/components/charts/BarChart.tsx | 13 +- .../usage/OrganisationUsage.container.tsx | 247 +++++------------- .../OrganisationUsageMetrics.container.tsx | 13 +- .../usage/components/SingleSDKLabelsChart.tsx | 114 +------- frontend/web/styles/3rdParty/_index.scss | 1 - frontend/web/styles/3rdParty/_recharts.scss | 25 -- 6 files changed, 101 insertions(+), 312 deletions(-) delete mode 100644 frontend/web/styles/3rdParty/_recharts.scss diff --git a/frontend/web/components/charts/BarChart.tsx b/frontend/web/components/charts/BarChart.tsx index b4d0b838b924..3e745d794870 100644 --- a/frontend/web/components/charts/BarChart.tsx +++ b/frontend/web/components/charts/BarChart.tsx @@ -34,20 +34,30 @@ type BarChartProps = { * (e.g. numeric env ids) that need a human-readable label on display. */ seriesLabels?: Record + /** Fixed bar width in pixels. Default: recharts auto-sizes by available space. */ + barSize?: number + /** Render vertical grid lines (one per x tick). Default `true`. */ + verticalGrid?: boolean } const BarChart: FC = ({ + barSize, colorMap, data, series, seriesLabels, showLegend = false, + verticalGrid = true, xAxisInterval = 0, }) => { return ( - + = ({ dataKey={label} stackId='series' fill={colorMap[label]} + barSize={barSize} animationBegin={index * 80} animationDuration={600} animationEasing='ease-out' diff --git a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx index ee79c463c04c..e65b452f9395 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx @@ -1,21 +1,7 @@ -import Utils from 'common/utils/utils' -import React, { FC } from 'react' -import { - Bar, - BarChart, - ResponsiveContainer, - Tooltip as _Tooltip, - XAxis, - YAxis, - CartesianGrid, - TooltipProps, -} from 'recharts' +import React, { FC, useMemo } from 'react' import moment from 'moment' -import { - NameType, - ValueType, -} from 'recharts/types/component/DefaultTooltipContent' import { AggregateUsageDataItem } from 'common/types/responses' +import BarChart from 'components/charts/BarChart' import UsageAPIDefinitions from './components/UsageAPIDefinitions' type OrganisationUsageProps = { @@ -25,13 +11,71 @@ type OrganisationUsageProps = { colours: string[] } +// Stable mapping between the user-facing selection label and the API field +// (and therefore the chart's dataKey). +const METRICS = [ + { dataKey: 'flags', label: 'Flags' }, + { dataKey: 'identities', label: 'Identities' }, + { dataKey: 'environment_document', label: 'Environment Document' }, + { dataKey: 'traits', label: 'Traits' }, +] as const + +type MetricDataKey = (typeof METRICS)[number]['dataKey'] + const OrganisationUsage: FC = ({ chartData, colours, isError, selection, }) => { - return chartData || isError ? ( + const formattedData = useMemo( + () => + chartData.map((d) => ({ + ...d, + day: moment(d.day).format('D MMM'), + })), + [chartData], + ) + + const series = useMemo( + () => + METRICS.filter((m) => selection.includes(m.label)).map((m) => m.dataKey), + [selection], + ) + + // dataKey → its colour at the metric's index in METRICS, so colours stay + // stable per metric regardless of which selections are active. Typed as + // `Record` so adding a new entry to METRICS forces this + // (and seriesLabels below) to be updated — TS will fail compilation if any + // dataKey is missing. + const colorMap = useMemo>( + () => ({ + environment_document: colours[2], + flags: colours[0], + identities: colours[1], + traits: colours[3], + }), + [colours], + ) + + // dataKey → display name (so the tooltip says "Environment Document" + // instead of "environment_document"). + const seriesLabels: Record = { + environment_document: 'Environment Document', + flags: 'Flags', + identities: 'Identities', + traits: 'Traits', + } + + if (!chartData && !isError) { + return ( +
+ +
+ ) + } + + return ( <> {isError || chartData?.length === 0 ? (
@@ -40,169 +84,18 @@ const OrganisationUsage: FC = ({ : 'No usage recorded.'}
) : ( - - - - 31 ? 7 : 0} - height={120} - angle={-90} - textAnchor='end' - tickFormatter={(v) => moment(v).format('D MMM')} - axisLine={{ stroke: '#EFF1F4' }} - tick={{ dx: -4, fill: '#656D7B' }} - tickLine={false} - /> - - <_Tooltip - cursor={{ fill: 'transparent' }} - content={} - /> - {selection.includes('Flags') && ( - - )} - {selection.includes('Identities') && ( - - )} - {selection.includes('Environment Document') && ( - - )} - {selection.includes('Traits') && ( - - )} - - + 31 ? 7 : 0} + barSize={14} + verticalGrid={false} + /> )} - ) : ( -
- -
- ) -} - -const RechartsTooltip: FC> = ({ - active, - label, - payload, -}) => { - if (!active || !payload || payload.length === 0) { - return null - } - - return ( -
-
- {moment(label).format('D MMM')} -
-
- {payload.map((el: any) => { - const { dataKey, fill, payload } = el - switch (dataKey) { - case 'traits': { - return ( - - - - Traits: {Utils.numberWithCommas(payload[dataKey])} - - - ) - } - case 'flags': { - return ( - - - - Flags: {Utils.numberWithCommas(payload[dataKey])} - - - ) - } - case 'identities': { - return ( - - - - Identities: {Utils.numberWithCommas(payload[dataKey])} - - - ) - } - case 'environment_document': { - return ( - - - - Environment Document:{' '} - {Utils.numberWithCommas(payload[dataKey])} - - - ) - } - default: { - return null - } - } - })} -
) } diff --git a/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx b/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx index d2049d9cc695..8a07d7c450a1 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useState } from 'react' +import moment from 'moment' import { Res } from 'common/types/responses' import SingleSDKLabelsChart from './components/SingleSDKLabelsChart' import { MultiSelect } from 'components/base/select/multi-select' @@ -46,7 +47,7 @@ const OrganisationUsageMetrics: React.FC = ({ return { aggregateChartData: [], allUserAgents: [], - userAgentColorMap: new Map(), + userAgentColorMap: {} as Record, } const aggregateGrouped: Record = {} @@ -54,7 +55,9 @@ const OrganisationUsageMetrics: React.FC = ({ const userAgentSet = new Set() data.events_list.forEach((event) => { - const date = event.day + // BarChart consumes the displayed day string as the x-axis dataKey, + // so format here once and let the chart use it verbatim. + const date = moment(event.day).format('D MMM') const userAgent = event.labels?.user_agent || 'Unknown' if (!userAgentSet.has(userAgent)) { @@ -79,9 +82,9 @@ const OrganisationUsageMetrics: React.FC = ({ (aggregateGrouped[date][userAgent] || 0) + totalForUserAgent }) - const colorMap = new Map() + const colorMap: Record = {} userAgents.forEach((agent, index) => { - colorMap.set(agent, colours[index % colours.length]) + colorMap[agent] = colours[index % colours.length] }) return { @@ -124,9 +127,7 @@ const OrganisationUsageMetrics: React.FC = ({ diff --git a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx index a83f447ff378..fbeb283d951a 100644 --- a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx +++ b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx @@ -1,120 +1,30 @@ import React, { FC } from 'react' -import { - Bar, - BarChart, - ResponsiveContainer, - Tooltip as _Tooltip, - XAxis, - YAxis, - CartesianGrid, - TooltipProps, - Legend, -} from 'recharts' -import moment from 'moment' -import { - NameType, - ValueType, -} from 'recharts/types/component/DefaultTooltipContent' +import BarChart from 'components/charts/BarChart' import { ChartDataPoint } from 'components/organisation-settings/usage/OrganisationUsageMetrics.container' -import Utils from 'common/utils/utils' -interface UsageChartProps { - colours: string[] +interface SingleSDKLabelsChartProps { title: string data: ChartDataPoint[] userAgents?: string[] - userAgentsColorMap: Map - metricKey: string + userAgentsColorMap: Record } -const UsageChart: React.FC = ({ - colours, +const SingleSDKLabelsChart: FC = ({ data, - metricKey, title, userAgents = [], userAgentsColorMap, }) => (
{title}
- - - - <_Tooltip - cursor={{ fill: 'transparent' }} - content={} - /> - moment(v).format('D MMM')} - interval={data?.length > 31 ? 7 : 0} - textAnchor='end' - axisLine={{ stroke: '#EFF1F4' }} - tick={{ dx: -4, fill: '#656D7B' }} - angle={-90} - tickLine={false} - allowDataOverflow={false} - /> - - - {userAgents?.map((userAgent, index) => ( - - ))} - - + 31 ? 7 : 0} + showLegend + />
) -const RechartsTooltip: FC> = ({ - active, - label, - payload, -}) => { - if (!active || !payload || payload.length === 0) { - return null - } - - return ( -
-
- {moment(label).format('D MMM')} -
-
- {payload.map((el: any) => { - const { dataKey, fill, value } = el - return ( - - - - {dataKey}: {Utils.numberWithCommas(value)} - - - ) - })} -
- ) -} - -export default UsageChart +export default SingleSDKLabelsChart diff --git a/frontend/web/styles/3rdParty/_index.scss b/frontend/web/styles/3rdParty/_index.scss index b921d2249cf1..423ed6ff9a0a 100644 --- a/frontend/web/styles/3rdParty/_index.scss +++ b/frontend/web/styles/3rdParty/_index.scss @@ -4,4 +4,3 @@ @import "react-datepicker"; @import "hw-badge"; @import "react-diff"; -@import "recharts"; diff --git a/frontend/web/styles/3rdParty/_recharts.scss b/frontend/web/styles/3rdParty/_recharts.scss deleted file mode 100644 index 2d4aa25aff3a..000000000000 --- a/frontend/web/styles/3rdParty/_recharts.scss +++ /dev/null @@ -1,25 +0,0 @@ -// Recharts global overrides. -// -// The new component passes colour tokens directly as prop values -// (e.g. `fill={colorTextSecondary}`) which browsers resolve in SVG -// presentation attributes — no CSS classname plumbing needed. -// -// TODO: delete everything below when SingleSDKLabelsChart and -// OrganisationUsage migrate to . The rules only exist to keep -// the two remaining raw-recharts consumers styled correctly. -.recharts-tooltip { - background-color: $text-icon-light; - border-radius: $border-radius-lg; -} - -.dark { - .recharts-tooltip-header { - color: $body-color; - } - - .recharts-wrapper { - line { - stroke: $bg-dark100; - } - } -} From 2cd7099997445db9953f5d1139d527b2f4032036 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 16 Apr 2026 12:19:34 -0300 Subject: [PATCH 2/5] fix: Replace `as never` with proper ChartDataPoint typing AggregateUsageDataItem has `number | null` fields; ChartDataPoint expects `number`. Explicitly coalesce nulls to 0 in the mapping instead of bypassing with a type assertion. Co-Authored-By: Claude Opus 4.6 --- .../usage/OrganisationUsage.container.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx index e65b452f9395..7458f959c3d8 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx @@ -1,7 +1,7 @@ import React, { FC, useMemo } from 'react' import moment from 'moment' import { AggregateUsageDataItem } from 'common/types/responses' -import BarChart from 'components/charts/BarChart' +import BarChart, { ChartDataPoint } from 'components/charts/BarChart' import UsageAPIDefinitions from './components/UsageAPIDefinitions' type OrganisationUsageProps = { @@ -28,11 +28,14 @@ const OrganisationUsage: FC = ({ isError, selection, }) => { - const formattedData = useMemo( + const formattedData: ChartDataPoint[] = useMemo( () => chartData.map((d) => ({ - ...d, day: moment(d.day).format('D MMM'), + environment_document: d.environment_document ?? 0, + flags: d.flags ?? 0, + identities: d.identities ?? 0, + traits: d.traits ?? 0, })), [chartData], ) @@ -85,7 +88,7 @@ const OrganisationUsage: FC = ({ ) : ( Date: Thu, 16 Apr 2026 13:12:36 -0300 Subject: [PATCH 3/5] refactor: Use EmptyState component for org usage empty/error states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the bare "No usage recorded." text with the reusable EmptyState component (icon + title + description) — consistent with the Feature Analytics chart empty state pattern. Co-Authored-By: Claude Opus 4.6 --- .../usage/OrganisationUsage.container.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx index 7458f959c3d8..0eab33f9ab28 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx @@ -1,6 +1,7 @@ import React, { FC, useMemo } from 'react' import moment from 'moment' import { AggregateUsageDataItem } from 'common/types/responses' +import EmptyState from 'components/EmptyState' import BarChart, { ChartDataPoint } from 'components/charts/BarChart' import UsageAPIDefinitions from './components/UsageAPIDefinitions' @@ -81,11 +82,15 @@ const OrganisationUsage: FC = ({ return ( <> {isError || chartData?.length === 0 ? ( -
- {isError - ? 'Your organisation does not have recurrent billing periods' - : 'No usage recorded.'} -
+ ) : ( Date: Thu, 16 Apr 2026 13:15:58 -0300 Subject: [PATCH 4/5] refactor: Add EmptyState to SingleSDKLabelsChart Show the EmptyState component instead of rendering an empty chart with bare axes when there's no SDK data. Co-Authored-By: Claude Opus 4.6 --- .../usage/components/SingleSDKLabelsChart.tsx | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx index fbeb283d951a..6075180708d4 100644 --- a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx +++ b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx @@ -1,5 +1,6 @@ import React, { FC } from 'react' import BarChart from 'components/charts/BarChart' +import EmptyState from 'components/EmptyState' import { ChartDataPoint } from 'components/organisation-settings/usage/OrganisationUsageMetrics.container' interface SingleSDKLabelsChartProps { @@ -14,17 +15,29 @@ const SingleSDKLabelsChart: FC = ({ title, userAgents = [], userAgentsColorMap, -}) => ( -
-
{title}
- 31 ? 7 : 0} - showLegend - /> -
-) +}) => { + const hasData = data.length > 0 && userAgents.length > 0 + + return ( +
+
{title}
+ {hasData ? ( + 31 ? 7 : 0} + showLegend + /> + ) : ( + + )} +
+ ) +} export default SingleSDKLabelsChart From e9c1a47aef9816e2f516eab1d85f4243813ec54c Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 24 Apr 2026 14:14:32 -0300 Subject: [PATCH 5/5] fix: address review comments on chart consolidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Defensive optional chaining on chartData in OrganisationUsage.container memo so the component is self-contained; parent always passes an array so no behaviour change. - Deduplicate ChartDataPoint — remove the copy in OrganisationUsageMetrics and have SingleSDKLabelsChart import from components/charts/BarChart. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../usage/OrganisationUsage.container.tsx | 4 ++-- .../usage/OrganisationUsageMetrics.container.tsx | 4 +--- .../usage/components/SingleSDKLabelsChart.tsx | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx index 0eab33f9ab28..a98db6fd613c 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx @@ -31,13 +31,13 @@ const OrganisationUsage: FC = ({ }) => { const formattedData: ChartDataPoint[] = useMemo( () => - chartData.map((d) => ({ + chartData?.map((d) => ({ day: moment(d.day).format('D MMM'), environment_document: d.environment_document ?? 0, flags: d.flags ?? 0, identities: d.identities ?? 0, traits: d.traits ?? 0, - })), + })) ?? [], [chartData], ) diff --git a/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx b/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx index 8a07d7c450a1..a6766c05084e 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsageMetrics.container.tsx @@ -3,14 +3,12 @@ import moment from 'moment' import { Res } from 'common/types/responses' import SingleSDKLabelsChart from './components/SingleSDKLabelsChart' import { MultiSelect } from 'components/base/select/multi-select' +import { ChartDataPoint } from 'components/charts/BarChart' interface OrganisationUsageMetricsProps { data?: Res['organisationUsage'] selectedMetrics: string[] } -export type ChartDataPoint = { - day: string -} & Record const colours = [ 'rgba(37, 99, 235, 0.8)', diff --git a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx index 6075180708d4..53ad7720f989 100644 --- a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx +++ b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx @@ -1,7 +1,6 @@ import React, { FC } from 'react' -import BarChart from 'components/charts/BarChart' +import BarChart, { ChartDataPoint } from 'components/charts/BarChart' import EmptyState from 'components/EmptyState' -import { ChartDataPoint } from 'components/organisation-settings/usage/OrganisationUsageMetrics.container' interface SingleSDKLabelsChartProps { title: string