Skip to content
Merged
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
82 changes: 82 additions & 0 deletions frontend/web/components/StatItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { FC, KeyboardEvent } from 'react'
import { IonIcon } from '@ionic/react'
import { checkmarkSharp } from 'ionicons/icons'
import Icon, { IconName } from './Icon'
import Utils from 'common/utils/utils'

type VisibilityToggleProps = {
colour: string
isVisible: boolean
onToggle: () => void
}

export type StatItemProps = {
icon: IconName
label: string
value: string | number
// Optional: for displaying limits (e.g., "1,000 / 10,000")
limit?: number | null
// Optional: for visibility toggle in charts
visibilityToggle?: VisibilityToggleProps
}

const StatItem: FC<StatItemProps> = ({
icon,
label,
limit,
value,
visibilityToggle,
}) => {
const formattedValue =
typeof value === 'number' ? Utils.numberWithCommas(value) : value

const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
visibilityToggle?.onToggle()
}
}

return (
<div className='d-flex flex-row align-items-start gap-2'>
<div className='plan-icon flex-shrink-0'>
<Icon name={icon} width={32} fill='#1A2634' />
</div>
<div>
<p className='fs-small lh-sm mb-0'>{label}</p>
<h4 className='mb-0'>
{formattedValue}
{limit !== null && limit !== undefined && (
<span className='text-muted fs-small fw-normal'>
{' '}
/ {Utils.numberWithCommas(limit)}
</span>
)}
</h4>
{visibilityToggle && (
<div
role='checkbox'
aria-checked={visibilityToggle.isVisible}
aria-label={`Toggle ${label} visibility`}
tabIndex={0}
className='cursor-pointer d-flex align-items-center gap-2 mt-1'
onClick={visibilityToggle.onToggle}
onKeyDown={handleKeyDown}
>
<div
className='visibility-checkbox'
style={{ backgroundColor: visibilityToggle.colour }}
>
{visibilityToggle.isVisible && (
<IonIcon size={'8px'} color='white' icon={checkmarkSharp} />
)}
</div>
<span className='text-muted fs-small'>Visible</span>
</div>
)}
</div>
</div>
)
}

export default StatItem
Original file line number Diff line number Diff line change
@@ -1,54 +1,14 @@
import React, { FC } from 'react'
import Utils from 'common/utils/utils'
import { IonIcon } from '@ionic/react'
import { checkmarkSharp } from 'ionicons/icons'
import { Res } from 'common/types/responses'
import { IconName } from 'components/Icon'
import StatItem from 'components/StatItem'

type LegendItemType = {
type TotalItem = {
colour: string | undefined
icon: IconName
limit: number | null | undefined
title: string
value: number
selection: string[]
onChange: (v: string) => void
colour?: string
}

const LegendItem: FC<LegendItemType> = ({
colour,
onChange,
selection,
title,
value,
}) => {
if (!value) {
return null
}
return (
<div className='mb-4'>
<h3 className='mb-2'>{Utils.numberWithCommas(value)}</h3>
<div
className='cursor-pointer d-flex align-items-center gap-2'
onClick={() => onChange(title)}
>
{!!colour && (
<div
className='text-white d-flex align-items-center justify-content-center'
style={{
backgroundColor: colour,
borderRadius: 2,
flexShrink: 0,
height: 16,
width: 16,
}}
>
{selection.includes(title) && (
<IonIcon size={'8px'} color='white' icon={checkmarkSharp} />
)}
</div>
)}
<span className='text-muted'>{title}</span>
</div>
</div>
)
}

export interface UsageChartTotalsProps {
Expand All @@ -57,11 +17,13 @@ export interface UsageChartTotalsProps {
updateSelection: (key: string) => void
colours: string[]
withColor?: boolean
maxApiCalls?: number | null
}

const UsageChartTotals: FC<UsageChartTotalsProps> = ({
colours,
data,
maxApiCalls,
selection,
updateSelection,
withColor = true,
Expand All @@ -70,47 +32,67 @@ const UsageChartTotals: FC<UsageChartTotalsProps> = ({
return null
}

const totalItems = [
const totalItems: TotalItem[] = [
{
colour: colours[0],
icon: 'features',
limit: undefined,
title: 'Flags',
value: data.totals.flags,
},
{
colour: colours[1],
icon: 'person',
limit: undefined,
title: 'Identities',
value: data.totals.identities,
},
{
colour: colours[2],
icon: 'file-text',
limit: undefined,
title: 'Environment Document',
value: data.totals.environmentDocument,
},
{
colour: colours[3],
icon: 'layers',
limit: undefined,
title: 'Traits',
value: data.totals.traits,
},
{
colour: undefined,
icon: 'bar-chart',
limit: maxApiCalls,
title: 'Total API Calls',
value: data.totals.total,
},
]

return (
<div className='d-flex gap-5 align-items-start'>
{totalItems.map((item) => (
<LegendItem
key={item.title}
selection={selection}
onChange={updateSelection}
colour={!withColor && item.colour ? '#6837fc' : item.colour}
value={item.value}
title={item.title}
/>
))}
</div>
<Row className='plan p-4 mb-4 flex-wrap gap-4'>
{totalItems
.filter((item) => item.value)
.map((item) => (
<StatItem
key={item.title}
icon={item.icon}
label={item.title}
value={item.value}
limit={item.limit}
visibilityToggle={
withColor && item.colour
? {
colour: item.colour,
isVisible: selection.includes(item.title),
onToggle: () => updateSelection(item.title),
}
: undefined
}
/>
))}
</Row>
)
}

Expand Down
13 changes: 10 additions & 3 deletions frontend/web/components/pages/OrganisationUsagePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import AccountStore from 'common/stores/account-store'
import { planNames } from 'common/utils/utils'
import { Req } from 'common/types/requests'
import { useGetOrganisationUsageQuery } from 'common/services/useOrganisationUsage'
import { useGetSubscriptionMetadataQuery } from 'common/services/useSubscriptionMetadata'
import UsageChartFilters from 'components/organisation-settings/usage/components/UsageChartFilters'
import UsageChartTotals from 'components/organisation-settings/usage/components/UsageChartTotals'

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

const [chartsView, setChartsView] = useState<'global' | 'user-agents'>(
getInitialView(),
Expand Down Expand Up @@ -56,12 +57,17 @@ const OrganisationUsagePage: FC = () => {
{
billing_period: billingPeriod,
environmentId: environment,
organisationId: organisationId?.toString() || '',
organisationId: organisationId || 0,
projectId: project,
},
{ skip: !organisationId },
)

const { data: subscriptionMeta } = useGetSubscriptionMetadataQuery(
{ id: organisationId || 0 },
{ skip: !organisationId },
)

// Aggregate usage events by date, summing metrics across all client types
const chartData = useMemo(() => {
const consolidated = Object.values(
Expand Down Expand Up @@ -146,7 +152,7 @@ const OrganisationUsagePage: FC = () => {
)}
>
<UsageChartFilters
organisationId={organisationId?.toString() || ''}
organisationId={organisationId || 0}
project={project}
setProject={setProject}
environment={environment}
Expand All @@ -161,6 +167,7 @@ const OrganisationUsagePage: FC = () => {
updateSelection={updateSelection}
colours={colours}
withColor={chartsView !== 'user-agents'}
maxApiCalls={subscriptionMeta?.max_api_calls}
/>
{chartsView === 'user-agents' ? (
<OrganisationUsageMetrics data={data} selectedMetrics={selection} />
Expand Down
Loading
Loading