diff --git a/frontend/web/components/StatItem.tsx b/frontend/web/components/StatItem.tsx new file mode 100644 index 000000000000..8f19017e759b --- /dev/null +++ b/frontend/web/components/StatItem.tsx @@ -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 = ({ + icon, + label, + limit, + value, + visibilityToggle, +}) => { + const formattedValue = + typeof value === 'number' ? Utils.numberWithCommas(value) : value + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + visibilityToggle?.onToggle() + } + } + + return ( +
+
+ +
+
+

{label}

+

+ {formattedValue} + {limit !== null && limit !== undefined && ( + + {' '} + / {Utils.numberWithCommas(limit)} + + )} +

+ {visibilityToggle && ( +
+
+ {visibilityToggle.isVisible && ( + + )} +
+ Visible +
+ )} +
+
+ ) +} + +export default StatItem diff --git a/frontend/web/components/organisation-settings/usage/components/UsageChartTotals.tsx b/frontend/web/components/organisation-settings/usage/components/UsageChartTotals.tsx index 7e5f2409945f..e29a9d6f90c1 100644 --- a/frontend/web/components/organisation-settings/usage/components/UsageChartTotals.tsx +++ b/frontend/web/components/organisation-settings/usage/components/UsageChartTotals.tsx @@ -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 = ({ - colour, - onChange, - selection, - title, - value, -}) => { - if (!value) { - return null - } - return ( -
-

{Utils.numberWithCommas(value)}

-
onChange(title)} - > - {!!colour && ( -
- {selection.includes(title) && ( - - )} -
- )} - {title} -
-
- ) } export interface UsageChartTotalsProps { @@ -57,11 +17,13 @@ export interface UsageChartTotalsProps { updateSelection: (key: string) => void colours: string[] withColor?: boolean + maxApiCalls?: number | null } const UsageChartTotals: FC = ({ colours, data, + maxApiCalls, selection, updateSelection, withColor = true, @@ -70,47 +32,67 @@ const UsageChartTotals: FC = ({ 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 ( -
- {totalItems.map((item) => ( - - ))} -
+ + {totalItems + .filter((item) => item.value) + .map((item) => ( + updateSelection(item.title), + } + : undefined + } + /> + ))} + ) } diff --git a/frontend/web/components/pages/OrganisationUsagePage.tsx b/frontend/web/components/pages/OrganisationUsagePage.tsx index f34f067887c7..9e3df27809f4 100644 --- a/frontend/web/components/pages/OrganisationUsagePage.tsx +++ b/frontend/web/components/pages/OrganisationUsagePage.tsx @@ -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' @@ -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(), @@ -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( @@ -146,7 +152,7 @@ const OrganisationUsagePage: FC = () => { )} > { updateSelection={updateSelection} colours={colours} withColor={chartsView !== 'user-agents'} + maxApiCalls={subscriptionMeta?.max_api_calls} /> {chartsView === 'user-agents' ? ( diff --git a/frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx b/frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx index 63dcf9392bc0..0531f1f7d18f 100644 --- a/frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx +++ b/frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx @@ -4,26 +4,76 @@ import Icon from 'components/Icon' import Utils from 'common/utils/utils' import Payment from 'components/modals/Payment' import { useGetSubscriptionMetadataQuery } from 'common/services/useSubscriptionMetadata' +import StatItem, { StatItemProps } from 'components/StatItem' type BillingTabProps = { organisation: Organisation } +type LimitItem = Pick & { value: string } + export const BillingTab = ({ organisation }: BillingTabProps) => { const { data: subscriptionMeta } = useGetSubscriptionMetadataQuery({ - id: String(organisation.id), + id: organisation.id, }) - const { chargebee_email } = subscriptionMeta || {} + const { + audit_log_visibility_days, + chargebee_email, + feature_history_visibility_days, + max_api_calls, + max_projects, + max_seats, + } = subscriptionMeta || {} const planName = Utils.getPlanName(organisation.subscription?.plan) || 'Free' + const formatLimit = (value: number | null | undefined): string => { + if (value === null || value === undefined) return 'Unlimited' + return Utils.numberWithCommas(value) + } + + const formatDays = (value: number | null | undefined): string => { + if (value === null || value === undefined) return 'Unlimited' + if (value === 0) return 'Not available' + return `${value} days` + } + + const showAuditLog = audit_log_visibility_days !== 0 + const showFeatureHistory = + Utils.getFlagsmithHasFeature('feature_versioning') && + feature_history_visibility_days !== 0 + + const limitItems: LimitItem[] = [ + { + icon: 'bar-chart', + label: 'API Calls', + value: formatLimit(max_api_calls), + }, + { icon: 'people', label: 'Team Seats', value: formatLimit(max_seats) }, + { icon: 'layers', label: 'Projects', value: formatLimit(max_projects) }, + showAuditLog + ? { + icon: 'list', + label: 'Audit Log', + value: formatDays(audit_log_visibility_days), + } + : undefined, + showFeatureHistory + ? { + icon: 'clock', + label: 'Feature History', + value: formatDays(feature_history_visibility_days), + } + : undefined, + ].filter((item): item is LimitItem => item !== undefined) + return (
- +
- +
- +
@@ -34,7 +84,7 @@ export const BillingTab = ({ organisation }: BillingTabProps) => {
- +

ID @@ -61,7 +111,7 @@ export const BillingTab = ({ organisation }: BillingTabProps) => { )}

-
+
{organisation.subscription?.subscription_id && (
+ {subscriptionMeta && ( + <> +
Subscription Limits
+ + {limitItems.map((item) => ( + + ))} + + + )}
Manage Payment Plan
diff --git a/frontend/web/styles/project/_panel.scss b/frontend/web/styles/project/_panel.scss index 521ec8da3f3c..b661e5aeb6de 100644 --- a/frontend/web/styles/project/_panel.scss +++ b/frontend/web/styles/project/_panel.scss @@ -95,6 +95,17 @@ } } +.visibility-checkbox { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 16px; + height: 16px; + border-radius: 2px; + color: white; +} + /*Change text in autofill textbox*/ .dark { input:-webkit-autofill,