Skip to content

Commit 7e2d9fb

Browse files
feat(frontend): add subscription limit info to Usage and Billing pages (#6539)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 00085cd commit 7e2d9fb

5 files changed

Lines changed: 216 additions & 69 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { FC, KeyboardEvent } from 'react'
2+
import { IonIcon } from '@ionic/react'
3+
import { checkmarkSharp } from 'ionicons/icons'
4+
import Icon, { IconName } from './Icon'
5+
import Utils from 'common/utils/utils'
6+
7+
type VisibilityToggleProps = {
8+
colour: string
9+
isVisible: boolean
10+
onToggle: () => void
11+
}
12+
13+
export type StatItemProps = {
14+
icon: IconName
15+
label: string
16+
value: string | number
17+
// Optional: for displaying limits (e.g., "1,000 / 10,000")
18+
limit?: number | null
19+
// Optional: for visibility toggle in charts
20+
visibilityToggle?: VisibilityToggleProps
21+
}
22+
23+
const StatItem: FC<StatItemProps> = ({
24+
icon,
25+
label,
26+
limit,
27+
value,
28+
visibilityToggle,
29+
}) => {
30+
const formattedValue =
31+
typeof value === 'number' ? Utils.numberWithCommas(value) : value
32+
33+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
34+
if (e.key === 'Enter' || e.key === ' ') {
35+
e.preventDefault()
36+
visibilityToggle?.onToggle()
37+
}
38+
}
39+
40+
return (
41+
<div className='d-flex flex-row align-items-start gap-2'>
42+
<div className='plan-icon flex-shrink-0'>
43+
<Icon name={icon} width={32} fill='#1A2634' />
44+
</div>
45+
<div>
46+
<p className='fs-small lh-sm mb-0'>{label}</p>
47+
<h4 className='mb-0'>
48+
{formattedValue}
49+
{limit !== null && limit !== undefined && (
50+
<span className='text-muted fs-small fw-normal'>
51+
{' '}
52+
/ {Utils.numberWithCommas(limit)}
53+
</span>
54+
)}
55+
</h4>
56+
{visibilityToggle && (
57+
<div
58+
role='checkbox'
59+
aria-checked={visibilityToggle.isVisible}
60+
aria-label={`Toggle ${label} visibility`}
61+
tabIndex={0}
62+
className='cursor-pointer d-flex align-items-center gap-2 mt-1'
63+
onClick={visibilityToggle.onToggle}
64+
onKeyDown={handleKeyDown}
65+
>
66+
<div
67+
className='visibility-checkbox'
68+
style={{ backgroundColor: visibilityToggle.colour }}
69+
>
70+
{visibilityToggle.isVisible && (
71+
<IonIcon size={'8px'} color='white' icon={checkmarkSharp} />
72+
)}
73+
</div>
74+
<span className='text-muted fs-small'>Visible</span>
75+
</div>
76+
)}
77+
</div>
78+
</div>
79+
)
80+
}
81+
82+
export default StatItem

frontend/web/components/organisation-settings/usage/components/UsageChartTotals.tsx

Lines changed: 41 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,14 @@
11
import React, { FC } from 'react'
2-
import Utils from 'common/utils/utils'
3-
import { IonIcon } from '@ionic/react'
4-
import { checkmarkSharp } from 'ionicons/icons'
52
import { Res } from 'common/types/responses'
3+
import { IconName } from 'components/Icon'
4+
import StatItem from 'components/StatItem'
65

7-
type LegendItemType = {
6+
type TotalItem = {
7+
colour: string | undefined
8+
icon: IconName
9+
limit: number | null | undefined
810
title: string
911
value: number
10-
selection: string[]
11-
onChange: (v: string) => void
12-
colour?: string
13-
}
14-
15-
const LegendItem: FC<LegendItemType> = ({
16-
colour,
17-
onChange,
18-
selection,
19-
title,
20-
value,
21-
}) => {
22-
if (!value) {
23-
return null
24-
}
25-
return (
26-
<div className='mb-4'>
27-
<h3 className='mb-2'>{Utils.numberWithCommas(value)}</h3>
28-
<div
29-
className='cursor-pointer d-flex align-items-center gap-2'
30-
onClick={() => onChange(title)}
31-
>
32-
{!!colour && (
33-
<div
34-
className='text-white d-flex align-items-center justify-content-center'
35-
style={{
36-
backgroundColor: colour,
37-
borderRadius: 2,
38-
flexShrink: 0,
39-
height: 16,
40-
width: 16,
41-
}}
42-
>
43-
{selection.includes(title) && (
44-
<IonIcon size={'8px'} color='white' icon={checkmarkSharp} />
45-
)}
46-
</div>
47-
)}
48-
<span className='text-muted'>{title}</span>
49-
</div>
50-
</div>
51-
)
5212
}
5313

5414
export interface UsageChartTotalsProps {
@@ -57,11 +17,13 @@ export interface UsageChartTotalsProps {
5717
updateSelection: (key: string) => void
5818
colours: string[]
5919
withColor?: boolean
20+
maxApiCalls?: number | null
6021
}
6122

6223
const UsageChartTotals: FC<UsageChartTotalsProps> = ({
6324
colours,
6425
data,
26+
maxApiCalls,
6527
selection,
6628
updateSelection,
6729
withColor = true,
@@ -70,47 +32,67 @@ const UsageChartTotals: FC<UsageChartTotalsProps> = ({
7032
return null
7133
}
7234

73-
const totalItems = [
35+
const totalItems: TotalItem[] = [
7436
{
7537
colour: colours[0],
38+
icon: 'features',
39+
limit: undefined,
7640
title: 'Flags',
7741
value: data.totals.flags,
7842
},
7943
{
8044
colour: colours[1],
45+
icon: 'person',
46+
limit: undefined,
8147
title: 'Identities',
8248
value: data.totals.identities,
8349
},
8450
{
8551
colour: colours[2],
52+
icon: 'file-text',
53+
limit: undefined,
8654
title: 'Environment Document',
8755
value: data.totals.environmentDocument,
8856
},
8957
{
9058
colour: colours[3],
59+
icon: 'layers',
60+
limit: undefined,
9161
title: 'Traits',
9262
value: data.totals.traits,
9363
},
9464
{
9565
colour: undefined,
66+
icon: 'bar-chart',
67+
limit: maxApiCalls,
9668
title: 'Total API Calls',
9769
value: data.totals.total,
9870
},
9971
]
10072

10173
return (
102-
<div className='d-flex gap-5 align-items-start'>
103-
{totalItems.map((item) => (
104-
<LegendItem
105-
key={item.title}
106-
selection={selection}
107-
onChange={updateSelection}
108-
colour={!withColor && item.colour ? '#6837fc' : item.colour}
109-
value={item.value}
110-
title={item.title}
111-
/>
112-
))}
113-
</div>
74+
<Row className='plan p-4 mb-4 flex-wrap gap-4'>
75+
{totalItems
76+
.filter((item) => item.value)
77+
.map((item) => (
78+
<StatItem
79+
key={item.title}
80+
icon={item.icon}
81+
label={item.title}
82+
value={item.value}
83+
limit={item.limit}
84+
visibilityToggle={
85+
withColor && item.colour
86+
? {
87+
colour: item.colour,
88+
isVisible: selection.includes(item.title),
89+
onToggle: () => updateSelection(item.title),
90+
}
91+
: undefined
92+
}
93+
/>
94+
))}
95+
</Row>
11496
)
11597
}
11698

frontend/web/components/pages/OrganisationUsagePage.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import AccountStore from 'common/stores/account-store'
1212
import { planNames } from 'common/utils/utils'
1313
import { Req } from 'common/types/requests'
1414
import { useGetOrganisationUsageQuery } from 'common/services/useOrganisationUsage'
15+
import { useGetSubscriptionMetadataQuery } from 'common/services/useSubscriptionMetadata'
1516
import UsageChartFilters from 'components/organisation-settings/usage/components/UsageChartFilters'
1617
import UsageChartTotals from 'components/organisation-settings/usage/components/UsageChartTotals'
1718

@@ -27,7 +28,7 @@ const OrganisationUsagePage: FC = () => {
2728
}
2829
const params = new URLSearchParams(location.search)
2930
return params.get('p') === 'user-agents' ? 'user-agents' : 'global'
30-
}, [location.search])
31+
}, [isSdkViewEnabled, location.search])
3132

3233
const [chartsView, setChartsView] = useState<'global' | 'user-agents'>(
3334
getInitialView(),
@@ -56,12 +57,17 @@ const OrganisationUsagePage: FC = () => {
5657
{
5758
billing_period: billingPeriod,
5859
environmentId: environment,
59-
organisationId: organisationId?.toString() || '',
60+
organisationId: organisationId || 0,
6061
projectId: project,
6162
},
6263
{ skip: !organisationId },
6364
)
6465

66+
const { data: subscriptionMeta } = useGetSubscriptionMetadataQuery(
67+
{ id: organisationId || 0 },
68+
{ skip: !organisationId },
69+
)
70+
6571
// Aggregate usage events by date, summing metrics across all client types
6672
const chartData = useMemo(() => {
6773
const consolidated = Object.values(
@@ -146,7 +152,7 @@ const OrganisationUsagePage: FC = () => {
146152
)}
147153
>
148154
<UsageChartFilters
149-
organisationId={organisationId?.toString() || ''}
155+
organisationId={organisationId || 0}
150156
project={project}
151157
setProject={setProject}
152158
environment={environment}
@@ -161,6 +167,7 @@ const OrganisationUsagePage: FC = () => {
161167
updateSelection={updateSelection}
162168
colours={colours}
163169
withColor={chartsView !== 'user-agents'}
170+
maxApiCalls={subscriptionMeta?.max_api_calls}
164171
/>
165172
{chartsView === 'user-agents' ? (
166173
<OrganisationUsageMetrics data={data} selectedMetrics={selection} />

0 commit comments

Comments
 (0)