Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion frontend/web/components/charts/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,30 @@ type BarChartProps = {
* (e.g. numeric env ids) that need a human-readable label on display.
*/
seriesLabels?: Record<string, string>
/** 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<BarChartProps> = ({
barSize,
colorMap,
data,
series,
seriesLabels,
showLegend = false,
verticalGrid = true,
xAxisInterval = 0,
}) => {
return (
<ResponsiveContainer height={400} width='100%'>
<RawBarChart data={data}>
<CartesianGrid strokeDasharray='3 5' strokeOpacity={0.4} />
<CartesianGrid
strokeDasharray='3 5'
strokeOpacity={0.4}
vertical={verticalGrid}
/>
<XAxis
dataKey='day'
padding='gap'
Expand Down Expand Up @@ -84,6 +94,7 @@ const BarChart: FC<BarChartProps> = ({
dataKey={label}
stackId='series'
fill={colorMap[label]}
barSize={barSize}
animationBegin={index * 80}
animationDuration={600}
animationEasing='ease-out'
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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<OrganisationUsageProps> = ({
chartData,
colours,
isError,
selection,
}) => {
return chartData || isError ? (
<>
{isError || chartData?.length === 0 ? (
<div className='py-4 fw-semibold text-center'>
{isError
? 'Your organisation does not have recurrent billing periods'
: 'No usage recorded.'}
</div>
) : (
<ResponsiveContainer height={400} width='100%'>
<BarChart data={chartData} style={{ stroke: '#fff', strokeWidth: 1 }}>
<CartesianGrid stroke='#EFF1F4' vertical={false} />
<XAxis
padding='gap'
allowDataOverflow={false}
dataKey='day'
interval={chartData?.length > 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}
/>
<YAxis
allowDataOverflow={false}
tickLine={false}
axisLine={{ stroke: '#EFF1F4' }}
tick={{ fill: '#1A2634' }}
/>
<_Tooltip
cursor={{ fill: 'transparent' }}
content={<RechartsTooltip />}
/>
{selection.includes('Flags') && (
<Bar dataKey='flags' barSize={14} stackId='a' fill={colours[0]} />
)}
{selection.includes('Identities') && (
<Bar
dataKey='identities'
barSize={14}
stackId='a'
fill={colours[1]}
/>
)}
{selection.includes('Environment Document') && (
<Bar
name='Environment Document'
dataKey='environment_document'
stackId='a'
fill={colours[2]}
barSize={14}
/>
)}
{selection.includes('Traits') && (
<Bar
dataKey='traits'
barSize={14}
stackId='a'
fill={colours[3]}
/>
)}
</BarChart>
</ResponsiveContainer>
)}
<UsageAPIDefinitions />
</>
) : (
<div className='text-center'>
<Loader />
</div>
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<TooltipProps<ValueType, NameType>> = ({
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<MetricDataKey, …>` 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<Record<MetricDataKey, string>>(
() => ({
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<MetricDataKey, string> = {
environment_document: 'Environment Document',
flags: 'Flags',
identities: 'Identities',
traits: 'Traits',
}

return (
<div className='recharts-tooltip py-2'>
<div className='px-4 py-2 fs-small lh-sm fw-bold recharts-tooltip-header'>
{moment(label).format('D MMM')}
if (!chartData && !isError) {
return (
<div className='text-center'>
<Loader />
</div>
<hr className='py-0 my-0 mb-3' />
{payload.map((el: any) => {
const { dataKey, fill, payload } = el
switch (dataKey) {
case 'traits': {
return (
<Row key={dataKey} className='px-4 mb-3'>
<span
style={{
backgroundColor: fill,
borderRadius: 2,
display: 'inline-block',
height: 16,
width: 16,
}}
/>
<span className='text-muted ml-2'>
Traits: {Utils.numberWithCommas(payload[dataKey])}
</span>
</Row>
)
}
case 'flags': {
return (
<Row key={dataKey} className='px-4 mb-3'>
<span
style={{
backgroundColor: fill,
borderRadius: 2,
display: 'inline-block',
height: 16,
width: 16,
}}
/>
<span className='text-muted ml-2'>
Flags: {Utils.numberWithCommas(payload[dataKey])}
</span>
</Row>
)
}
case 'identities': {
return (
<Row key={dataKey} className='px-4 mb-3'>
<span
style={{
backgroundColor: fill,
borderRadius: 2,
display: 'inline-block',
height: 16,
width: 16,
}}
/>
<span className='text-muted ml-2'>
Identities: {Utils.numberWithCommas(payload[dataKey])}
</span>
</Row>
)
}
case 'environment_document': {
return (
<Row key={dataKey} className='px-4 mb-3'>
<span
style={{
backgroundColor: fill,
borderRadius: 2,
display: 'inline-block',
height: 16,
width: 16,
}}
/>
<span className='text-muted ml-2'>
Environment Document:{' '}
{Utils.numberWithCommas(payload[dataKey])}
</span>
</Row>
)
}
default: {
return null
)
}

return (
<>
{isError || chartData?.length === 0 ? (
<EmptyState
title={isError ? 'No billing periods' : 'No usage recorded'}
description={
isError
? 'Your organisation does not have recurrent billing periods.'
: 'No usage data available for the selected period and project.'
}
}
})}
</div>
icon='bar-chart'
/>
) : (
<BarChart
data={formattedData}
series={series}
colorMap={colorMap}
seriesLabels={seriesLabels}
xAxisInterval={chartData?.length > 31 ? 7 : 0}
barSize={14}
verticalGrid={false}
/>
)}
<UsageAPIDefinitions />
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -46,15 +47,17 @@ const OrganisationUsageMetrics: React.FC<OrganisationUsageMetricsProps> = ({
return {
aggregateChartData: [],
allUserAgents: [],
userAgentColorMap: new Map(),
userAgentColorMap: {} as Record<string, string>,
}

const aggregateGrouped: Record<string, ChartDataPoint> = {}
const userAgents: string[] = []
const userAgentSet = new Set<string>()

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)) {
Expand All @@ -79,9 +82,9 @@ const OrganisationUsageMetrics: React.FC<OrganisationUsageMetricsProps> = ({
(aggregateGrouped[date][userAgent] || 0) + totalForUserAgent
})

const colorMap = new Map()
const colorMap: Record<string, string> = {}
userAgents.forEach((agent, index) => {
colorMap.set(agent, colours[index % colours.length])
colorMap[agent] = colours[index % colours.length]
})

return {
Expand Down Expand Up @@ -124,9 +127,7 @@ const OrganisationUsageMetrics: React.FC<OrganisationUsageMetricsProps> = ({
<SingleSDKLabelsChart
data={aggregateChartData}
userAgents={filteredUserAgents}
metricKey='aggregate'
title='API Usage'
colours={colours}
userAgentsColorMap={userAgentColorMap}
/>
</div>
Expand Down
Loading
Loading