Add billing usage analytics dashboard with ClickHouse integration (#18592)

## Summary
This PR adds a comprehensive billing usage analytics feature that
provides detailed breakdowns of credit consumption across execution
types, users, resources, and time periods. The implementation includes a
new ClickHouse-backed analytics service, GraphQL API endpoint, and a
frontend dashboard component.

## Key Changes

### Backend
- **New BillingAnalyticsService**: Queries ClickHouse for usage
breakdowns by user, resource, execution type, and time series data
- **BillingEventWriterService**: Writes billing events to ClickHouse for
analytics while maintaining best-effort semantics (never blocks Stripe
billing)
- **ClickHouse Schema**: Added `billingEvent` table with 3-year TTL for
storing detailed billing event data
- **GraphQL Resolver**: New `getBillingAnalytics` query that aggregates
usage data for the current billing period, protected by feature flag and
billing permissions
- **Enhanced BillingUsageEvent**: Added `userWorkspaceId` field to track
per-user credit consumption
- **AI Billing Integration**: Updated AI billing service to pass
`userWorkspaceId` when recording usage events

### Frontend
- **SettingsBillingAnalyticsSection**: New component displaying:
  - Usage breakdown by execution type with progress bars
  - Daily usage time series chart (28-day view)
  - Per-user credit consumption breakdown
  - Per-resource (agent/workflow) credit consumption breakdown
- **SettingsUsage Page**: Dedicated page for viewing usage analytics
- **GraphQL Query**: `GetBillingAnalytics` query with generated hooks
- **Navigation**: Added Usage menu item in settings (feature-flagged)
- **Mock Data**: Included screenshot mock data for preview/testing

### Feature Flag
- Added `IS_USAGE_ANALYTICS_ENABLED` feature flag to control visibility
and access to analytics features

## Implementation Details
- Analytics data is queried in parallel for performance
- ClickHouse writes are non-blocking to ensure billing operations never
fail
- Progress bars use dynamic coloring from a predefined palette
- Time series visualization normalizes bar heights relative to max value
- Empty state handling when no analytics data is available
- Responsive UI with proper text truncation for long names

https://claude.ai/code/session_01Y1EqrX6PFq3EJxJq89h7DF

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-03-23 10:28:23 +01:00
committed by GitHub
parent 49af539032
commit 77d4bd9158
154 changed files with 2246 additions and 565 deletions
@@ -10,13 +10,11 @@ module.exports = {
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/ai/graphql/**/*.{ts,tsx}',
'./src/modules/applications/graphql/**/*.{ts,tsx}',
'./src/modules/application-variables/graphql/**/*.{ts,tsx}',
'./src/modules/workspace/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-member/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-invitation/graphql/**/*.{ts,tsx}',
'./src/modules/billing/graphql/**/*.{ts,tsx}',
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
import { AIChatBanner } from '@/ai/components/AIChatBanner';
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
@@ -13,7 +13,7 @@ import {
agentChatUsageState,
type AgentChatLastMessageUsage,
} from '@/ai/states/agentChatUsageState';
import { SettingsBillingLabelValueItem } from '@/billing/components/internal/SettingsBillingLabelValueItem';
import { SettingsBillingLabelValueItem } from '@/settings/billing/components/internal/SettingsBillingLabelValueItem';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { formatNumber } from '~/utils/format/formatNumber';
@@ -259,6 +259,18 @@ const SettingsBilling = lazy(() =>
})),
);
const SettingsUsage = lazy(() =>
import('~/pages/settings/SettingsUsage').then((module) => ({
default: module.SettingsUsage,
})),
);
const SettingsUsageUserDetail = lazy(() =>
import('~/pages/settings/SettingsUsageUserDetail').then((module) => ({
default: module.SettingsUsageUserDetail,
})),
);
const SettingsObjects = lazy(() =>
import('~/pages/settings/data-model/SettingsObjects').then((module) => ({
default: module.SettingsObjects,
@@ -529,6 +541,19 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
element={<SettingsLogicFunctionDetail />}
/>
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
<Route
element={
<SettingsProtectedRouteWrapper
requiredFeatureFlag={FeatureFlagKey.IS_USAGE_ANALYTICS_ENABLED}
/>
}
>
<Route path={SettingsPath.Usage} element={<SettingsUsage />} />
<Route
path={SettingsPath.UsageUserDetail}
element={<SettingsUsageUserDetail />}
/>
</Route>
<Route
path={SettingsPath.Subdomain}
element={<SettingsSubdomainPage />}
@@ -4,7 +4,7 @@ import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessio
import { returnToPathState } from '@/auth/states/returnToPathState';
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import deepEqual from 'deep-equal';
import { useStore } from 'jotai';
@@ -1,5 +1,5 @@
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const billingCheckoutSessionState =
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { returnToPathState } from '@/auth/states/returnToPathState';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { isNonEmptyString } from '@sniptt/guards';
import { useStore } from 'jotai';
@@ -1,4 +1,4 @@
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { useLingui } from '@lingui/react/macro';
@@ -1,5 +1,5 @@
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/settings/billing/constants/BillingCheckoutSessionDefaultValue';
import { useHandleCheckoutSession } from '@/settings/billing/hooks/useHandleCheckoutSession';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { t } from '@lingui/core/macro';
@@ -168,6 +168,7 @@ export const useSaveLayoutCustomization = () => {
saveFieldsWidgetGroups,
exitLayoutCustomizationMode,
enqueueErrorSnackBar,
isRecordPageLayoutEditingEnabled,
store,
t,
]);
@@ -1,8 +0,0 @@
export type Opportunity = {
__typename: 'Opportunity';
id: string;
createdAt: string;
updatedAt?: string;
deletedAt?: string | null;
name: string | null;
};

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 365 KiB

@@ -1,22 +1,22 @@
import { useLingui } from '@lingui/react/macro';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsBillingCreditsSection } from '@/billing/components/SettingsBillingCreditsSection';
import { SettingsBillingSubscriptionInfo } from '@/billing/components/SettingsBillingSubscriptionInfo';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { SettingsBillingCreditsSection } from '@/settings/billing/components/SettingsBillingCreditsSection';
import { SettingsBillingSubscriptionInfo } from '@/settings/billing/components/SettingsBillingSubscriptionInfo';
import { useGetWorkflowNodeExecutionUsage } from '@/settings/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { useQuery } from '@apollo/client/react';
import { isDefined } from 'twenty-shared/utils';
import { H2Title, IconCircleX, IconCreditCard } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useQuery } from '@apollo/client/react';
import {
SubscriptionStatus,
BillingPortalSessionDocument,
SubscriptionStatus,
} from '~/generated-metadata/graphql';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const SettingsBillingContent = () => {
const { t } = useLingui();
@@ -1,21 +1,33 @@
import { type CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { MeteredPriceSelector } from '@/billing/components/internal/MeteredPriceSelector';
import { SettingsBillingLabelValueItem } from '@/billing/components/internal/SettingsBillingLabelValueItem';
import { SubscriptionInfoContainer } from '@/billing/components/SubscriptionInfoContainer';
import { useBillingWording } from '@/billing/hooks/useBillingWording';
import { useCurrentBillingFlags } from '@/billing/hooks/useCurrentBillingFlags';
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { MeteredPriceSelector } from '@/settings/billing/components/internal/MeteredPriceSelector';
import { SettingsBillingLabelValueItem } from '@/settings/billing/components/internal/SettingsBillingLabelValueItem';
import { SubscriptionInfoContainer } from '@/settings/billing/components/SubscriptionInfoContainer';
import { useBillingWording } from '@/settings/billing/hooks/useBillingWording';
import { useCurrentBillingFlags } from '@/settings/billing/hooks/useCurrentBillingFlags';
import { useCurrentMetered } from '@/settings/billing/hooks/useCurrentMetered';
import { useGetWorkflowNodeExecutionUsage } from '@/settings/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { formatToShortNumber } from 'twenty-shared/utils';
import { H2Title, HorizontalSeparator } from 'twenty-ui/display';
import { SettingsPath } from 'twenty-shared/types';
import { formatToShortNumber, getSettingsPath } from 'twenty-shared/utils';
import { H2Title, HorizontalSeparator, IconChartBar } from 'twenty-ui/display';
import { ProgressBar } from 'twenty-ui/feedback';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { SubscriptionStatus } from '~/generated-metadata/graphql';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import {
FeatureFlagKey,
SubscriptionStatus,
} from '~/generated-metadata/graphql';
const StyledCreditUsageFooterActions = styled.div`
margin-top: ${themeCssVariables.spacing[4]};
`;
export const SettingsBillingCreditsSection = ({
currentBillingSubscription,
@@ -32,6 +44,10 @@ export const SettingsBillingCreditsSection = ({
const { getCurrentMeteredPricesByInterval } = useCurrentMetered();
const isUsageAnalyticsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_USAGE_ANALYTICS_ENABLED,
);
const { getIntervalLabel } = useBillingWording();
const isTrialing = subscriptionStatus === SubscriptionStatus.Trialing;
@@ -127,6 +143,18 @@ export const SettingsBillingCreditsSection = ({
</>
)}
</SubscriptionInfoContainer>
{isUsageAnalyticsEnabled && (
<StyledCreditUsageFooterActions>
<UndecoratedLink to={getSettingsPath(SettingsPath.Usage)}>
<Button
Icon={IconChartBar}
title={t`View usage`}
variant="secondary"
/>
</UndecoratedLink>
</StyledCreditUsageFooterActions>
)}
</Section>
<Section>
<MeteredPriceSelector
@@ -1,26 +1,26 @@
import { SubscriptionInfoContainer } from '@/billing/components/SubscriptionInfoContainer';
import { SubscriptionInfoContainer } from '@/settings/billing/components/SubscriptionInfoContainer';
import {
SubscriptionInfoHeaderRow,
SubscriptionInfoRowContainer,
} from '@/billing/components/internal/SubscriptionInfoRowContainer';
} from '@/settings/billing/components/internal/SubscriptionInfoRowContainer';
import {
type CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { PlansTags } from '@/billing/components/internal/PlansTags';
import { useBillingWording } from '@/billing/hooks/useBillingWording';
import { useCurrentBillingFlags } from '@/billing/hooks/useCurrentBillingFlags';
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useHasNextBillingPhase } from '@/billing/hooks/useHasNextBillingPhase';
import { useNextBillingPhase } from '@/billing/hooks/useNextBillingPhase';
import { useNextBillingSeats } from '@/billing/hooks/useNextBillingSeats';
import { useNextPlan } from '@/billing/hooks/useNextPlan';
import { useSplitPhaseItemsInPrices } from '@/billing/hooks/useSplitPhaseItemsInPrices';
import { PlansTags } from '@/settings/billing/components/internal/PlansTags';
import { useBillingWording } from '@/settings/billing/hooks/useBillingWording';
import { useCurrentBillingFlags } from '@/settings/billing/hooks/useCurrentBillingFlags';
import { useCurrentMetered } from '@/settings/billing/hooks/useCurrentMetered';
import { useCurrentPlan } from '@/settings/billing/hooks/useCurrentPlan';
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
import { useGetWorkflowNodeExecutionUsage } from '@/settings/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useHasNextBillingPhase } from '@/settings/billing/hooks/useHasNextBillingPhase';
import { useNextBillingPhase } from '@/settings/billing/hooks/useNextBillingPhase';
import { useNextBillingSeats } from '@/settings/billing/hooks/useNextBillingSeats';
import { useNextPlan } from '@/settings/billing/hooks/useNextPlan';
import { useSplitPhaseItemsInPrices } from '@/settings/billing/hooks/useSplitPhaseItemsInPrices';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -5,6 +5,7 @@ const StyledSubscriptionInfoContainer = styled.div`
background-color: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[3]};
@@ -1,11 +1,11 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useBillingWording } from '@/billing/hooks/useBillingWording';
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useBillingWording } from '@/settings/billing/hooks/useBillingWording';
import { useCurrentMetered } from '@/settings/billing/hooks/useCurrentMetered';
import { useGetWorkflowNodeExecutionUsage } from '@/settings/billing/hooks/useGetWorkflowNodeExecutionUsage';
import {
type BillingPriceTiers,
type MeteredBillingPrice,
} from '@/billing/types/billing-price-tiers.type';
} from '@/settings/billing/types/billing-price-tiers.type';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -1,6 +1,6 @@
import { gql } from '@apollo/client';
import { BILLING_PRICE_METERED_FRAGMENT } from '@/billing/graphql/fragments/billingPriceMeteredFragment';
import { BILLING_PRICE_LICENSED_FRAGMENT } from '@/billing/graphql/fragments/billingPriceLicensedFragment';
import { BILLING_PRICE_METERED_FRAGMENT } from '@/settings/billing/graphql/fragments/billingPriceMeteredFragment';
import { BILLING_PRICE_LICENSED_FRAGMENT } from '@/settings/billing/graphql/fragments/billingPriceLicensedFragment';
export const LIST_PLANS = gql`
query listPlans {
@@ -1,4 +1,4 @@
import { useAllBillingPrices } from '@/billing/hooks/useAllBillingPrices';
import { useAllBillingPrices } from '@/settings/billing/hooks/useAllBillingPrices';
describe('useAllBillingPrices', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/settings/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
describe('useBaseLicensedPriceByPlanKeyAndInterval', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useBaseProductByPlanKey } from '@/billing/hooks/useBaseProductByPlanKey';
import { useBaseProductByPlanKey } from '@/settings/billing/hooks/useBaseProductByPlanKey';
describe('useBaseProductByPlanKey', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useBillingWording } from '@/billing/hooks/useBillingWording';
import { useBillingWording } from '@/settings/billing/hooks/useBillingWording';
describe('useBillingWording', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useCurrentBillingFlags } from '@/billing/hooks/useCurrentBillingFlags';
import { useCurrentBillingFlags } from '@/settings/billing/hooks/useCurrentBillingFlags';
describe('useCurrentBillingFlags', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useCurrentMetered } from '@/settings/billing/hooks/useCurrentMetered';
describe('useCurrentMetered', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import { useCurrentPlan } from '@/settings/billing/hooks/useCurrentPlan';
describe('useCurrentPlan', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useEndSubscriptionTrialPeriod } from '@/billing/hooks/useEndSubscriptionTrialPeriod';
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
describe('useEndSubscriptionTrialPeriod', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useFormatPrices } from '@/billing/hooks/useFormatPrices';
import { useFormatPrices } from '@/settings/billing/hooks/useFormatPrices';
describe('useFormatPrices', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useGetWorkflowNodeExecutionUsage } from '@/settings/billing/hooks/useGetWorkflowNodeExecutionUsage';
describe('useGetWorkflowNodeExecutionUsage', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
import { useHandleCheckoutSession } from '@/settings/billing/hooks/useHandleCheckoutSession';
describe('useHandleCheckoutSession', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useHasNextBillingPhase } from '@/billing/hooks/useHasNextBillingPhase';
import { useHasNextBillingPhase } from '@/settings/billing/hooks/useHasNextBillingPhase';
describe('useHasNextBillingPhase', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useListProducts } from '@/billing/hooks/useListProducts';
import { useListProducts } from '@/settings/billing/hooks/useListProducts';
describe('useListProducts', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useNextBillingPhase } from '@/billing/hooks/useNextBillingPhase';
import { useNextBillingPhase } from '@/settings/billing/hooks/useNextBillingPhase';
describe('useNextBillingPhase', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useNextBillingSeats } from '@/billing/hooks/useNextBillingSeats';
import { useNextBillingSeats } from '@/settings/billing/hooks/useNextBillingSeats';
describe('useNextBillingSeats', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useNextPlan } from '@/billing/hooks/useNextPlan';
import { useNextPlan } from '@/settings/billing/hooks/useNextPlan';
describe('useNextPlan', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { usePlanByPlanKey } from '@/billing/hooks/usePlanByPlanKey';
import { usePlanByPlanKey } from '@/settings/billing/hooks/usePlanByPlanKey';
describe('usePlanByPlanKey', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { usePlanByPriceId } from '@/billing/hooks/usePlanByPriceId';
import { usePlanByPriceId } from '@/settings/billing/hooks/usePlanByPriceId';
describe('usePlanByPriceId', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { usePlans } from '@/billing/hooks/usePlans';
import { usePlans } from '@/settings/billing/hooks/usePlans';
describe('usePlans', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { usePriceAndBillingUsageByPriceId } from '@/billing/hooks/usePriceAndBillingUsageByPriceId';
import { usePriceAndBillingUsageByPriceId } from '@/settings/billing/hooks/usePriceAndBillingUsageByPriceId';
describe('usePriceAndBillingUsageByPriceId', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { useSplitPhaseItemsInPrices } from '@/billing/hooks/useSplitPhaseItemsInPrices';
import { useSplitPhaseItemsInPrices } from '@/settings/billing/hooks/useSplitPhaseItemsInPrices';
describe('useSplitPhaseItemsInPrices', () => {
it('should be a function', () => {
@@ -1,4 +1,4 @@
import { usePlans } from '@/billing/hooks/usePlans';
import { usePlans } from '@/settings/billing/hooks/usePlans';
import {
type BillingPriceLicensed,
type BillingPriceMetered,
@@ -3,7 +3,7 @@ import {
type BillingPlanKey,
} from '~/generated-metadata/graphql';
import { findOrThrow } from 'twenty-shared/utils';
import { useBaseProductByPlanKey } from '@/billing/hooks/useBaseProductByPlanKey';
import { useBaseProductByPlanKey } from '@/settings/billing/hooks/useBaseProductByPlanKey';
export const useBaseLicensedPriceByPlanKeyAndInterval = () => {
const { getBaseProductByPlanKey } = useBaseProductByPlanKey();
@@ -3,7 +3,7 @@ import {
type BillingPlanKey,
} from '~/generated-metadata/graphql';
import { findOrThrow } from 'twenty-shared/utils';
import { usePlanByPlanKey } from '@/billing/hooks/usePlanByPlanKey';
import { usePlanByPlanKey } from '@/settings/billing/hooks/usePlanByPlanKey';
export const useBaseProductByPlanKey = () => {
const { getPlanByPlanKey } = usePlanByPlanKey();
@@ -1,4 +1,4 @@
import { useFormatPrices } from '@/billing/hooks/useFormatPrices';
import { useFormatPrices } from '@/settings/billing/hooks/useFormatPrices';
import {
BillingPlanKey,
SubscriptionInterval,
@@ -9,9 +9,9 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { useLingui } from '@lingui/react/macro';
import { beautifyExactDate } from '~/utils/date-utils';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useCurrentBillingFlags } from '@/billing/hooks/useCurrentBillingFlags';
import { useCurrentPlan } from '@/settings/billing/hooks/useCurrentPlan';
import { useCurrentMetered } from '@/settings/billing/hooks/useCurrentMetered';
import { useCurrentBillingFlags } from '@/settings/billing/hooks/useCurrentBillingFlags';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useBillingWording = () => {
@@ -1,6 +1,6 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import type { MeteredBillingPrice } from '@/billing/types/billing-price-tiers.type';
import { useCurrentPlan } from '@/settings/billing/hooks/useCurrentPlan';
import type { MeteredBillingPrice } from '@/settings/billing/types/billing-price-tiers.type';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { assertIsDefinedOrThrow, findOrThrow } from 'twenty-shared/utils';
import {
@@ -2,7 +2,7 @@ import {
BillingPlanKey,
SubscriptionInterval,
} from '~/generated-metadata/graphql';
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/settings/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
export const useFormatPrices = () => {
const { getBaseLicensedPriceByPlanKeyAndInterval } =
@@ -1,4 +1,4 @@
import { usePlans } from '@/billing/hooks/usePlans';
import { usePlans } from '@/settings/billing/hooks/usePlans';
export const useListProducts = () => {
const { listPlans } = usePlans();
@@ -1,6 +1,6 @@
import { findOrThrow, isDefined } from 'twenty-shared/utils';
import { useSplitPhaseItemsInPrices } from '@/billing/hooks/useSplitPhaseItemsInPrices';
import { useNextBillingPhase } from '@/billing/hooks/useNextBillingPhase';
import { useSplitPhaseItemsInPrices } from '@/settings/billing/hooks/useSplitPhaseItemsInPrices';
import { useNextBillingPhase } from '@/settings/billing/hooks/useNextBillingPhase';
export const useNextBillingSeats = () => {
const { splitedPhaseItemsInPrices } = useSplitPhaseItemsInPrices();
@@ -1,5 +1,5 @@
import { useSplitPhaseItemsInPrices } from '@/billing/hooks/useSplitPhaseItemsInPrices';
import { usePlanByPriceId } from '@/billing/hooks/usePlanByPriceId';
import { useSplitPhaseItemsInPrices } from '@/settings/billing/hooks/useSplitPhaseItemsInPrices';
import { usePlanByPriceId } from '@/settings/billing/hooks/usePlanByPriceId';
export const useNextPlan = () => {
const { splitedPhaseItemsInPrices } = useSplitPhaseItemsInPrices();
@@ -1,6 +1,6 @@
import { useNextBillingPhase } from '@/billing/hooks/useNextBillingPhase';
import { usePriceAndBillingUsageByPriceId } from '@/billing/hooks/usePriceAndBillingUsageByPriceId';
import { type MeteredBillingPrice } from '@/billing/types/billing-price-tiers.type';
import { useNextBillingPhase } from '@/settings/billing/hooks/useNextBillingPhase';
import { usePriceAndBillingUsageByPriceId } from '@/settings/billing/hooks/usePriceAndBillingUsageByPriceId';
import { type MeteredBillingPrice } from '@/settings/billing/types/billing-price-tiers.type';
import {
BillingUsageType,
type BillingPriceLicensed,
@@ -1,6 +1,6 @@
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
import { SubscriptionBenefit } from '@/settings/billing/components/SubscriptionBenefit';
import { ENTERPRISE_CHECKOUT_SESSION } from '@/settings/enterprise/graphql/queries/enterpriseCheckoutSession';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
@@ -22,6 +22,7 @@ import {
IconCalendarEvent,
IconColorSwatch,
type IconComponent,
IconChartBar,
IconCurrencyDollar,
IconDoorEnter,
IconHelpCircle,
@@ -78,6 +79,9 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const isApplicationEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_APPLICATION_ENABLED,
);
const isUsageAnalyticsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_USAGE_ANALYTICS_ENABLED,
);
const isSupportChatConfigured =
supportChat?.supportDriver === 'FRONT' &&
isNonEmptyString(supportChat.supportFrontChatId);
@@ -161,6 +165,15 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
isHidden:
!isBillingEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`Usage`,
path: SettingsPath.Usage,
Icon: IconChartBar,
isHidden:
!isUsageAnalyticsEnabled ||
isBillingEnabled ||
!permissionMap[PermissionFlagType.WORKSPACE],
},
{
label: t`APIs & Webhooks`,
path: SettingsPath.ApiWebhooks,
@@ -0,0 +1,266 @@
import { SettingsBillingLabelValueItem } from '@/settings/billing/components/internal/SettingsBillingLabelValueItem';
import { SubscriptionInfoContainer } from '@/settings/billing/components/SubscriptionInfoContainer';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { GraphWidgetLineChart } from '@/page-layout/widgets/graph/graph-widget-line-chart/components/GraphWidgetLineChart';
import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graph-widget-line-chart/types/LineChartSeriesWithColor';
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
import { getColorSchemeByIndex } from '@/page-layout/widgets/graph/utils/getColorSchemeByIndex';
import { Select } from '@/ui/input/components/Select';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { getOperationTypeLabel } from '@/settings/usage/utils/getOperationTypeLabel';
import { getPeriodDates } from '@/settings/usage/utils/getPeriodDates';
import { getPeriodOptions } from '@/settings/usage/utils/getPeriodOptions';
import { type PeriodPreset } from '@/settings/usage/utils/periodPreset';
import { UsagePieChart } from '@/settings/usage/components/UsagePieChart';
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { useContext, useState } from 'react';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { Avatar, H2Title, IconChevronRight } from 'twenty-ui/display';
import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useQuery } from '@apollo/client/react';
import { GetUsageAnalyticsDocument } from '~/generated-metadata/graphql';
import { formatDate } from '~/utils/date-utils';
import { normalizeSearchText } from '~/utils/normalizeSearchText';
const StyledSearchInputContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledIconChevronRightContainer = styled.div`
color: ${themeCssVariables.font.color.tertiary};
`;
const StyledLineChartContainer = styled.div`
height: 200px;
width: 100%;
`;
const USAGE_USER_TABLE_GRID_TEMPLATE_COLUMNS = '1fr 120px 36px';
export const SettingsUsageAnalyticsSection = () => {
const { theme } = useContext(ThemeContext);
const { formatNumber } = useNumberFormat();
const [typePeriod, setTypePeriod] = useState<PeriodPreset>('30d');
const [dailyPeriod, setDailyPeriod] = useState<PeriodPreset>('30d');
const [userPeriod, setUserPeriod] = useState<PeriodPreset>('30d');
const [userSearchTerm, setUserSearchTerm] = useState('');
const colorRegistry = createGraphColorRegistry(theme.color);
const periodOptions = getPeriodOptions();
const typeDates = getPeriodDates(typePeriod);
const dailyDates = getPeriodDates(dailyPeriod);
const userDates = getPeriodDates(userPeriod);
const { data: typeData, loading: typeLoading } = useQuery(
GetUsageAnalyticsDocument,
{ variables: { input: typeDates } },
);
const { data: dailyData, loading: dailyLoading } = useQuery(
GetUsageAnalyticsDocument,
{ variables: { input: dailyDates } },
);
const { data: userData, loading: userLoading } = useQuery(
GetUsageAnalyticsDocument,
{ variables: { input: userDates } },
);
const typeAnalytics = typeData?.getUsageAnalytics;
const dailyAnalytics = dailyData?.getUsageAnalytics;
const userAnalytics = userData?.getUsageAnalytics;
const usageByOperationType = typeAnalytics?.usageByOperationType ?? [];
const timeSeries = dailyAnalytics?.timeSeries ?? [];
const usageByUser = userAnalytics?.usageByUser ?? [];
const anyLoading = typeLoading || dailyLoading || userLoading;
if (anyLoading) {
return null;
}
const hasAnyData =
usageByOperationType.length > 0 ||
timeSeries.length > 0 ||
usageByUser.length > 0;
const totalCredits = usageByOperationType.reduce(
(sum, item) => sum + item.creditsUsed,
0,
);
const filteredUsageByUser = usageByUser.filter((item) => {
const search = normalizeSearchText(userSearchTerm);
const name = normalizeSearchText(item.label ?? item.key);
return name.includes(search);
});
const pieData = usageByOperationType.map((item, index) => ({
id: getOperationTypeLabel(item.key),
value: item.creditsUsed,
color: getColorSchemeByIndex(colorRegistry, index).solid,
}));
const lineData: LineChartSeriesWithColor[] = [
{
id: 'credits',
label: t`Credits`,
data: timeSeries.map((point) => ({
x: formatDate(point.date, 'MMM d'),
y: point.creditsUsed,
})),
},
];
if (!hasAnyData) {
return (
<Section>
<H2Title
title={t`Usage Analytics`}
description={t`Credit usage breakdown for your workspace.`}
/>
<SubscriptionInfoContainer>
<SettingsBillingLabelValueItem
label={t`No usage data`}
value={t`No credit consumption recorded yet.`}
/>
</SubscriptionInfoContainer>
</Section>
);
}
return (
<>
{usageByOperationType.length > 0 && (
<Section>
<H2Title
title={t`Usage by Type`}
description={t`${formatNumber(totalCredits)} credits`}
adornment={
<Select
dropdownId="usage-type-period"
value={typePeriod}
options={periodOptions}
onChange={setTypePeriod}
needIconCheck
selectSizeVariant="small"
/>
}
/>
<SubscriptionInfoContainer>
<UsagePieChart data={pieData} />
</SubscriptionInfoContainer>
</Section>
)}
{timeSeries.length > 0 && (
<Section>
<H2Title
title={t`Daily Usage`}
description={t`Credit consumption over time.`}
adornment={
<Select
dropdownId="usage-daily-period"
value={dailyPeriod}
options={periodOptions}
onChange={setDailyPeriod}
needIconCheck
selectSizeVariant="small"
/>
}
/>
<SubscriptionInfoContainer>
<StyledLineChartContainer>
<GraphWidgetLineChart
id="usage-daily-line-chart"
data={lineData}
colorMode="automaticPalette"
showLegend={false}
enableArea
/>
</StyledLineChartContainer>
</SubscriptionInfoContainer>
</Section>
)}
{usageByUser.length > 0 && (
<Section>
<H2Title
title={t`Usage by User`}
description={t`Click a user to see their daily breakdown.`}
adornment={
<Select
dropdownId="usage-user-period"
value={userPeriod}
options={periodOptions}
onChange={setUserPeriod}
needIconCheck
selectSizeVariant="small"
/>
}
/>
<StyledSearchInputContainer>
<SearchInput
placeholder={t`Search for a user...`}
value={userSearchTerm}
onChange={setUserSearchTerm}
/>
</StyledSearchInputContainer>
<Table>
<TableRow
gridTemplateColumns={USAGE_USER_TABLE_GRID_TEMPLATE_COLUMNS}
>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Credits`}</TableHeader>
<TableHeader />
</TableRow>
{filteredUsageByUser.map((item) => (
<TableRow
key={item.key}
gridTemplateColumns={USAGE_USER_TABLE_GRID_TEMPLATE_COLUMNS}
to={getSettingsPath(SettingsPath.UsageUserDetail, {
userWorkspaceId: item.key,
})}
>
<TableCell
color={themeCssVariables.font.color.primary}
gap={themeCssVariables.spacing[2]}
>
<Avatar
type="rounded"
size="md"
placeholder={item.label ?? item.key}
placeholderColorSeed={item.key}
/>
{item.label ?? item.key}
</TableCell>
<TableCell align="right">
{formatNumber(item.creditsUsed)}
</TableCell>
<TableCell align="center">
<StyledIconChevronRightContainer>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledIconChevronRightContainer>
</TableCell>
</TableRow>
))}
</Table>
</Section>
)}
</>
);
};
@@ -0,0 +1,52 @@
import { CHART_MOTION_CONFIG } from '@/page-layout/widgets/graph/constants/ChartMotionConfig';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { ResponsivePie } from '@nivo/pie';
import { useContext } from 'react';
import { ThemeContext } from 'twenty-ui/theme-constants';
type UsagePieChartDatum = {
id: string;
value: number;
color: string;
};
type UsagePieChartProps = {
data: UsagePieChartDatum[];
};
const StyledContainer = styled.div`
height: 220px;
width: 100%;
`;
export const UsagePieChart = ({ data }: UsagePieChartProps) => {
const { theme } = useContext(ThemeContext);
const { formatNumber } = useNumberFormat();
return (
<StyledContainer>
<ResponsivePie
data={data}
margin={{ top: 20, right: 80, bottom: 20, left: 80 }}
innerRadius={0.6}
padAngle={0.5}
cornerRadius={2}
colors={data.map((item) => item.color)}
enableArcLabels={false}
enableArcLinkLabels={true}
arcLinkLabelsSkipAngle={10}
arcLinkLabelsTextColor={theme.font.color.secondary}
arcLinkLabelsColor={{ from: 'color' }}
arcLinkLabelsDiagonalLength={10}
arcLinkLabelsStraightLength={10}
animate
motionConfig={CHART_MOTION_CONFIG}
tooltip={({ datum }) => (
<div>{`${String(datum.id)}: ${t`${formatNumber(datum.value)} credits`}`}</div>
)}
/>
</StyledContainer>
);
};
@@ -0,0 +1,30 @@
import { gql } from '@apollo/client';
export const GET_USAGE_ANALYTICS = gql`
query GetUsageAnalytics($input: UsageAnalyticsInput) {
getUsageAnalytics(input: $input) {
usageByUser {
key
label
creditsUsed
}
usageByOperationType {
key
creditsUsed
}
timeSeries {
date
creditsUsed
}
periodStart
periodEnd
userDailyUsage {
userWorkspaceId
dailyUsage {
date
creditsUsed
}
}
}
}
`;
@@ -0,0 +1,14 @@
import { t } from '@lingui/core/macro';
export const getOperationTypeLabel = (key: string): string => {
switch (key) {
case 'AI_TOKEN':
return t`AI Chat`;
case 'WORKFLOW_EXECUTION':
return t`Workflow Execution`;
case 'CODE_EXECUTION':
return t`Code Execution`;
default:
return key;
}
};
@@ -0,0 +1,25 @@
import { type PeriodPreset } from '@/settings/usage/utils/periodPreset';
const PERIOD_DAYS: Record<PeriodPreset, number> = {
'7d': 7,
'30d': 30,
'90d': 90,
};
export const getPeriodDates = (
preset: PeriodPreset,
): { periodStart: string; periodEnd: string } => {
const now = new Date();
const end = new Date(now);
end.setHours(23, 59, 59, 999);
const start = new Date(end);
start.setDate(start.getDate() - PERIOD_DAYS[preset]);
start.setHours(0, 0, 0, 0);
return {
periodStart: start.toISOString(),
periodEnd: end.toISOString(),
};
};
@@ -0,0 +1,12 @@
import { t } from '@lingui/core/macro';
import { type PeriodPreset } from '@/settings/usage/utils/periodPreset';
export const getPeriodOptions = (): {
value: PeriodPreset;
label: string;
}[] => [
{ value: '7d', label: t`Last 7 days` },
{ value: '30d', label: t`Last 30 days` },
{ value: '90d', label: t`Last 90 days` },
];
@@ -0,0 +1 @@
export type PeriodPreset = '7d' | '30d' | '90d';
@@ -18,7 +18,7 @@ type TableCellProps = {
const StyledTableCell = styled.div<TableCellProps>`
align-items: center;
color: ${({ color }) => color || themeCssVariables.font.color.secondary};
cursor: ${({ clickable }) => (clickable === true ? 'pointer' : 'default')};
cursor: ${({ clickable }) => (clickable === true ? 'pointer' : 'inherit')};
display: flex;
gap: ${({ gap }) => gap ?? 'normal'};
height: ${({ height }) => height ?? themeCssVariables.spacing[8]};
@@ -1,6 +1,6 @@
import { gql } from '@apollo/client';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_ITEM_FRAGMENT } from '@/billing/graphql/fragments/billingSubscriptionSchedulePhaseItemFragment';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_FRAGMENT } from '@/billing/graphql/fragments/billingSubscriptionSchedulePhaseFragment';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_ITEM_FRAGMENT } from '@/settings/billing/graphql/fragments/billingSubscriptionSchedulePhaseItemFragment';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_FRAGMENT } from '@/settings/billing/graphql/fragments/billingSubscriptionSchedulePhaseFragment';
export const BILLING_SUBSCRIPTION_FRAGMENT = gql`
fragment BillingSubscriptionFragment on BillingSubscription {
@@ -1,6 +1,6 @@
import { gql } from '@apollo/client';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_FRAGMENT } from '@/billing/graphql/fragments/billingSubscriptionSchedulePhaseFragment';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_ITEM_FRAGMENT } from '@/billing/graphql/fragments/billingSubscriptionSchedulePhaseItemFragment';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_FRAGMENT } from '@/settings/billing/graphql/fragments/billingSubscriptionSchedulePhaseFragment';
import { BILLING_SUBSCRIPTION_SCHEDULE_PHASE_ITEM_FRAGMENT } from '@/settings/billing/graphql/fragments/billingSubscriptionSchedulePhaseItemFragment';
export const CURRENT_BILLING_SUBSCRIPTION_FRAGMENT = gql`
fragment CurrentBillingSubscriptionFragment on BillingSubscription {
@@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import { ChooseYourPlanContent } from '~/pages/onboarding/internal/ChooseYourPlanContent';
import { billingState } from '@/client-config/states/billingState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { usePlans } from '@/billing/hooks/usePlans';
import { usePlans } from '@/settings/billing/hooks/usePlans';
const StyledChooseYourPlanPlaceholder = styled.div`
height: 566px;
@@ -3,12 +3,12 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useAuth } from '@/auth/hooks/useAuth';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
import { SubscriptionPrice } from '@/billing/components/SubscriptionPrice';
import { TrialCard } from '@/billing/components/TrialCard';
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
import { useBaseProductByPlanKey } from '@/billing/hooks/useBaseProductByPlanKey';
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
import { SubscriptionBenefit } from '@/settings/billing/components/SubscriptionBenefit';
import { SubscriptionPrice } from '@/settings/billing/components/SubscriptionPrice';
import { TrialCard } from '@/settings/billing/components/TrialCard';
import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/settings/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval';
import { useBaseProductByPlanKey } from '@/settings/billing/hooks/useBaseProductByPlanKey';
import { useHandleCheckoutSession } from '@/settings/billing/hooks/useHandleCheckoutSession';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -2,10 +2,10 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { SettingsBillingContent } from '@/billing/components/SettingsBillingContent';
import { SettingsBillingContent } from '@/settings/billing/components/SettingsBillingContent';
import { getSettingsPath } from 'twenty-shared/utils';
import { SettingsPath } from 'twenty-shared/types';
import { usePlans } from '@/billing/hooks/usePlans';
import { usePlans } from '@/settings/billing/hooks/usePlans';
export const SettingsBilling = () => {
const { t } = useLingui();

Some files were not shown because too many files have changed in this diff Show More