Skip to content

Commit 085c0b9

Browse files
FelixMalfaitclaude
andauthored
feat(admin-panel): add read-only Billing tab and workspace logos (#20012)
## Summary - Adds a **Billing** tab on the admin-panel workspace detail page that surfaces Stripe customer + active subscription details (status, plan, interval, current period, trial, cancellation, line items, credit balance). Tab is gated on `IS_BILLING_ENABLED` both in the backend service and in the frontend tab list — completely hidden on instances where billing is disabled. - Renders a **workspace avatar next to the name** in the admin Top Workspaces list by plumbing the workspace `logo` field through the admin DTO, statistics SQL query, and generated admin GraphQL types. - **Read-only** by design: no Stripe API calls, no mutations — data comes from the existing \`BillingCustomerEntity\` / \`BillingSubscriptionEntity\` / \`BillingPriceEntity\` tables via \`BillingSubscriptionService.getCurrentBillingSubscription\`. ### What the tab shows - **Customer** container — Stripe customer ID (with link to the Stripe dashboard, monospaced), credit balance (formatted, from \`creditBalanceMicro\`). - **Subscription** container — status tag (color-coded), plan tag, billing interval, current period range, trial range (if trialing), \`cancelAtPeriodEnd\` / \`cancelAt\` / \`canceledAt\` (only when set), Stripe subscription ID (external link). - **Line items** — one card per subscription item with product name, product key tag, seats (if quantity), credits per period (for metered), unit price (formatted with currency). ### Design choices - Styling matches the user-facing billing page (\`SubscriptionInfoContainer\` + \`Tag\` + \`H2Title\` + \`Section\`) — no new UI primitives. - Currency is rendered inline with amounts via \`Intl.NumberFormat\` (e.g. \`\$19.00\`) instead of as a separate row. - Uses the generated admin GraphQL types (\`WorkspaceBillingAdminPanelQuery\`, \`SubscriptionStatus\`, \`SubscriptionInterval\`) — no hand-typed response shapes. ## Test plan - [x] \`npx nx typecheck twenty-server\` — passes - [x] \`npx nx typecheck twenty-front\` — passes - [x] oxlint + prettier on all touched files — clean - [x] \`graphql:generate --configuration=admin\` — regenerated; new \`workspaceBillingAdminPanel\` query + \`logo\` field on \`AdminPanelTopWorkspace\` appear in \`generated-admin/graphql.ts\` - [x] Backend GraphQL schema introspection shows \`workspaceBillingAdminPanel\` query on \`/admin-panel\` - [x] Direct GraphQL call with seeded \`BillingCustomer\` + \`BillingSubscription\` + \`BillingPrice\` rows returns the expected shape (\`status: "Trialing"\`, plan \`PRO\`, items with quantity/unitAmount/includedCredits, trial period dates) - [x] With \`IS_BILLING_ENABLED=false\` (default) the Billing tab is hidden — verified in the admin panel UI - [x] Top Workspaces list renders workspace avatars next to names — verified in the admin panel UI - [ ] Smoke test the Billing tab render in a real instance that has \`IS_BILLING_ENABLED=true\` + live Stripe data (skipped locally due to dev-env auth friction after toggling billing/multi-workspace; recommend a reviewer check) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9ce9e2b commit 085c0b9

15 files changed

Lines changed: 754 additions & 7 deletions

packages/twenty-front/src/generated-admin/graphql.ts

Lines changed: 67 additions & 2 deletions
Large diffs are not rendered by default.

packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSec
44
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
55
import { ADMIN_PANEL_RECENT_USERS } from '@/settings/admin-panel/graphql/queries/adminPanelRecentUsers';
66
import { ADMIN_PANEL_TOP_WORKSPACES } from '@/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces';
7+
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
78
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
89
import { Table } from '@/ui/layout/table/components/Table';
910
import { TableBody } from '@/ui/layout/table/components/TableBody';
@@ -14,21 +15,37 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
1415
import { useQuery } from '@apollo/client/react';
1516
import { styled } from '@linaria/react';
1617
import { t } from '@lingui/core/macro';
18+
import { isNonEmptyString } from '@sniptt/guards';
1719
import { useState } from 'react';
1820
import { useDebounce } from 'use-debounce';
1921
import { SettingsPath } from 'twenty-shared/types';
20-
import { getSettingsPath } from 'twenty-shared/utils';
22+
import { getImageAbsoluteURI, getSettingsPath } from 'twenty-shared/utils';
23+
import { AvatarOrIcon } from 'twenty-ui/components';
2124

2225
import { currentUserState } from '@/auth/states/currentUserState';
2326
import { H2Title } from 'twenty-ui/display';
2427
import { Section } from 'twenty-ui/layout';
2528
import { themeCssVariables } from 'twenty-ui/theme-constants';
29+
import { REACT_APP_SERVER_BASE_URL } from '~/config';
2630

2731
const StyledEmptyState = styled.div`
2832
color: ${themeCssVariables.font.color.tertiary};
2933
padding: ${themeCssVariables.spacing[4]} 0;
3034
`;
3135

36+
const StyledWorkspaceCell = styled.div`
37+
align-items: center;
38+
display: flex;
39+
gap: ${themeCssVariables.spacing[2]};
40+
min-width: 0;
41+
`;
42+
43+
const StyledWorkspaceName = styled.span`
44+
overflow: hidden;
45+
text-overflow: ellipsis;
46+
white-space: nowrap;
47+
`;
48+
3249
export const SettingsAdminGeneral = () => {
3350
const apolloAdminClient = useApolloAdminClient();
3451
const [userSearchTerm, setUserSearchTerm] = useState('');
@@ -64,6 +81,7 @@ export const SettingsAdminGeneral = () => {
6481
name: string;
6582
totalUsers: number;
6683
subdomain: string;
84+
logo: string | null;
6785
}[];
6886
}>(ADMIN_PANEL_TOP_WORKSPACES, {
6987
client: apolloAdminClient,
@@ -176,7 +194,23 @@ export const SettingsAdminGeneral = () => {
176194
)}
177195
>
178196
<TableCell color={themeCssVariables.font.color.primary}>
179-
{workspace.name || '\u2014'}
197+
<StyledWorkspaceCell>
198+
<AvatarOrIcon
199+
avatarUrl={
200+
getImageAbsoluteURI({
201+
imageUrl: isNonEmptyString(workspace.logo)
202+
? workspace.logo
203+
: DEFAULT_WORKSPACE_LOGO,
204+
baseUrl: REACT_APP_SERVER_BASE_URL,
205+
}) ?? ''
206+
}
207+
placeholder={workspace.name}
208+
avatarType="squared"
209+
/>
210+
<StyledWorkspaceName>
211+
{workspace.name || '\u2014'}
212+
</StyledWorkspaceName>
213+
</StyledWorkspaceCell>
180214
</TableCell>
181215
<TableCell align="right">
182216
{workspace.totalUsers}

0 commit comments

Comments
 (0)