fix: resolve settings/usage chart crash and add ClickHouse usage event seeds (#19039)
## Summary
- **Fix settings/usage page crash**: The `GraphWidgetLineChart`
component used on `settings/usage` was crashing with "Instance id is not
provided and cannot be found in context" because it requires
`WidgetComponentInstanceContext` (for tooltip/crosshair component
states) which is only provided inside the widget system. Wraps the
standalone chart usages with the required context provider.
- **Avoid mounting `GraphWidgetLegend` when hidden**: The legend
component calls `useIsPageLayoutInEditMode()` which requires
`PageLayoutEditModeProviderContext` — another context only available
inside the widget system. Since the settings page passes
`showLegend={false}`, the fix conditionally unmounts the legend instead
of always mounting it with a `show` prop. Applied consistently across
all four chart types (line, bar, pie, gauge).
- **Add ClickHouse usage event seeds**: Generates ~400 realistic
`usageEvent` rows spanning the past 35 days with weighted user activity,
weekday/weekend patterns, and gradual ramp-up. Enables developers to see
the usage analytics page with data locally.
## Test plan
- [ ] Navigate to `settings/usage` — page should render without errors
- [ ] Verify the daily usage line chart displays correctly
- [ ] Navigate to a user detail page from the usage list
- [ ] Verify the user detail chart renders without errors
- [ ] Run `npx nx clickhouse:seed twenty-server` and confirm usage
events are seeded
- [ ] Verify chart legend still works correctly on dashboard widgets (no
regression)
Made with [Cursor](https://cursor.com)
This commit is contained in:
+109
-1
@@ -1,11 +1,14 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { H2Title, IconBolt, IconLock, IconRobot } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||
import { SettingsAdminAiModelsTable } from '@/settings/admin-panel/ai/components/SettingsAdminAiModelsTable';
|
||||
import { SettingsAdminAiProviderListCard } from '@/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard';
|
||||
@@ -13,23 +16,51 @@ import { AI_PROVIDER_SOURCE } from '@/settings/admin-panel/ai/constants/AiProvid
|
||||
import { SET_ADMIN_AI_MODEL_RECOMMENDED } from '@/settings/admin-panel/ai/graphql/mutations/setAdminAiModelRecommended';
|
||||
import { SET_ADMIN_DEFAULT_AI_MODEL } from '@/settings/admin-panel/ai/graphql/mutations/setAdminDefaultAiModel';
|
||||
import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels';
|
||||
import { GET_ADMIN_AI_USAGE_BY_WORKSPACE } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiUsageByWorkspace';
|
||||
import { GET_AI_PROVIDERS } from '@/settings/admin-panel/ai/graphql/queries/getAiProviders';
|
||||
import { type GetAiProvidersResult } from '@/settings/admin-panel/ai/types/GetAiProvidersResult';
|
||||
import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon';
|
||||
import { parseProviderItems } from '@/settings/admin-panel/ai/utils/parseProviderItems';
|
||||
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
||||
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { useUsageValueFormatter } from '@/settings/usage/hooks/useUsageValueFormatter';
|
||||
import { getPeriodDates } from '@/settings/usage/utils/getPeriodDates';
|
||||
import { getPeriodOptions } from '@/settings/usage/utils/getPeriodOptions';
|
||||
import { type PeriodPreset } from '@/settings/usage/utils/periodPreset';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
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 { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import {
|
||||
AiModelRole,
|
||||
type AdminAiModelConfig,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const USAGE_TABLE_GRID_TEMPLATE_COLUMNS = '1fr 120px';
|
||||
|
||||
type UsageBreakdownItem = {
|
||||
key: string;
|
||||
label?: string | null;
|
||||
creditsUsed: number;
|
||||
};
|
||||
|
||||
export const SettingsAdminAI = () => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { refetch: refetchClientConfig } = useClientConfig();
|
||||
const { formatUsageValue } = useUsageValueFormatter();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const isBillingEnabled = billing?.isBillingEnabled ?? false;
|
||||
const hasEnterpriseAccess =
|
||||
isBillingEnabled || currentWorkspace?.hasValidEnterpriseKey === true;
|
||||
const [usagePeriod, setUsagePeriod] = useState<PeriodPreset>('30d');
|
||||
const periodOptions = getPeriodOptions();
|
||||
const usageDates = getPeriodDates(usagePeriod);
|
||||
|
||||
const { data, loading: isLoadingModels } = useQuery<{
|
||||
getAdminAiModels: {
|
||||
@@ -45,6 +76,19 @@ export const SettingsAdminAI = () => {
|
||||
const { data: providersData, loading: isLoadingProviders } =
|
||||
useQuery<GetAiProvidersResult>(GET_AI_PROVIDERS);
|
||||
|
||||
const { data: usageData, previousData: previousUsageData } = useQuery<{
|
||||
getAdminAiUsageByWorkspace: UsageBreakdownItem[];
|
||||
}>(GET_ADMIN_AI_USAGE_BY_WORKSPACE, {
|
||||
variables: {
|
||||
periodStart: usageDates.periodStart,
|
||||
periodEnd: usageDates.periodEnd,
|
||||
},
|
||||
skip: !hasEnterpriseAccess,
|
||||
});
|
||||
|
||||
const effectiveUsageData = usageData ?? previousUsageData;
|
||||
const usageByWorkspace = effectiveUsageData?.getAdminAiUsageByWorkspace ?? [];
|
||||
|
||||
const models = data?.getAdminAiModels?.models ?? [];
|
||||
|
||||
const providerItems = useMemo(
|
||||
@@ -208,6 +252,70 @@ export const SettingsAdminAI = () => {
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`AI Usage by Workspace`}
|
||||
description={t`AI consumption across all workspaces.`}
|
||||
adornment={
|
||||
hasEnterpriseAccess ? (
|
||||
<Select
|
||||
dropdownId="admin-ai-usage-period"
|
||||
value={usagePeriod}
|
||||
options={periodOptions}
|
||||
onChange={setUsagePeriod}
|
||||
needIconCheck
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
) : (
|
||||
<Tag
|
||||
text={t`Enterprise`}
|
||||
color="transparent"
|
||||
Icon={IconLock}
|
||||
variant="border"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{hasEnterpriseAccess ? (
|
||||
usageByWorkspace.length > 0 ? (
|
||||
<Table>
|
||||
<TableRow gridTemplateColumns={USAGE_TABLE_GRID_TEMPLATE_COLUMNS}>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader align="right">{t`Usage`}</TableHeader>
|
||||
</TableRow>
|
||||
{usageByWorkspace.map((item) => (
|
||||
<TableRow
|
||||
key={item.key}
|
||||
gridTemplateColumns={USAGE_TABLE_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{item.label ?? item.key}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatUsageValue(item.creditsUsed)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
) : (
|
||||
<Card rounded>
|
||||
<TableRow gridTemplateColumns="1fr">
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
align="center"
|
||||
>
|
||||
{t`No AI usage data recorded yet.`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Card>
|
||||
)
|
||||
) : (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
description={t`AI usage analytics across workspaces is available with an Enterprise key.`}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ADMIN_AI_USAGE_BY_WORKSPACE = gql`
|
||||
query GetAdminAiUsageByWorkspace(
|
||||
$periodStart: DateTime
|
||||
$periodEnd: DateTime
|
||||
) {
|
||||
getAdminAiUsageByWorkspace(
|
||||
periodStart: $periodStart
|
||||
periodEnd: $periodEnd
|
||||
) {
|
||||
key
|
||||
label
|
||||
creditsUsed
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user