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..0eab33f9ab28 100644 --- a/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx +++ b/frontend/web/components/organisation-settings/usage/OrganisationUsage.container.tsx @@ -1,21 +1,8 @@ -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 EmptyState from 'components/EmptyState' +import BarChart, { ChartDataPoint } from 'components/charts/BarChart' import UsageAPIDefinitions from './components/UsageAPIDefinitions' type OrganisationUsageProps = { @@ -25,184 +12,98 @@ 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 ? ( - <> - {isError || chartData?.length === 0 ? ( -
- {isError - ? 'Your organisation does not have recurrent billing periods' - : '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') && ( - - )} - - - )} - - - ) : ( -
- -
+ const formattedData: ChartDataPoint[] = useMemo( + () => + 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], ) -} -const RechartsTooltip: FC> = ({ - active, - label, - payload, -}) => { - if (!active || !payload || payload.length === 0) { - return null + 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', } - return ( -
-
- {moment(label).format('D MMM')} + if (!chartData && !isError) { + return ( +
+
-
- {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 + ) + } + + return ( + <> + {isError || chartData?.length === 0 ? ( + + icon='bar-chart' + /> + ) : ( + 31 ? 7 : 0} + barSize={14} + verticalGrid={false} + /> + )} + + ) } 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..6075180708d4 100644 --- a/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx +++ b/frontend/web/components/organisation-settings/usage/components/SingleSDKLabelsChart.tsx @@ -1,120 +1,43 @@ 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 EmptyState from 'components/EmptyState' 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) => ( - - ))} - - -
-) - -const RechartsTooltip: FC> = ({ - active, - label, - payload, }) => { - if (!active || !payload || payload.length === 0) { - return null - } + const hasData = data.length > 0 && userAgents.length > 0 return ( -
-
- {moment(label).format('D MMM')} -
-
- {payload.map((el: any) => { - const { dataKey, fill, value } = el - return ( - - - - {dataKey}: {Utils.numberWithCommas(value)} - - - ) - })} +
+
{title}
+ {hasData ? ( + 31 ? 7 : 0} + showLegend + /> + ) : ( + + )}
) } -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; - } - } -}