From 0078d27eb27913555a1403d6872e45b80620fea5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 22 Sep 2026 22:42:17 -0700 Subject: [PATCH 1/5] improvement(insights): serve org usage from settled segments and redesign the overview --- .../[id]/usage/{summary => overview}/route.ts | 15 +- .../[organizationId]/usage/route.test.ts | 6 +- .../components/activity-summary.tsx | 124 +++---- .../components/usage-consumers.tsx | 14 +- .../components/usage-credits.test.tsx | 48 +++ .../components/usage-credits.tsx | 176 ++++++++++ .../components/usage-member-avatar.tsx | 24 ++ .../components/usage-monitoring.tsx | 99 +++--- .../components/usage-source-mix.tsx | 50 --- .../components/usage-summary.test.tsx | 28 -- .../components/usage-summary.tsx | 76 ----- .../components/usage-top-cards.tsx | 104 ++++++ apps/sim/ee/organization-usage/constants.ts | 39 +++ .../hooks/use-legend-highlight.ts | 26 ++ apps/sim/hooks/queries/organization-usage.ts | 26 +- apps/sim/hooks/queries/organization.ts | 5 + .../queries/utils/organization-usage-keys.ts | 28 +- .../api/contracts/organization-activity.ts | 3 +- .../lib/api/contracts/organization-usage.ts | 42 ++- .../authorized-organization-usage-use-case.ts | 14 +- .../get-organization-activity.test.ts | 54 ++- .../get-organization-activity.ts | 12 +- .../get-organization-usage-breakdown.ts | 307 ++++++++++-------- .../get-organization-usage-overview.test.ts | 150 +++++++++ .../get-organization-usage-overview.ts | 185 +++++++++++ .../organization-usage/operations.ts | 8 + .../core/organization-activity-queries.ts | 139 ++++++-- .../core/organization-activity-summary.ts | 59 ++++ .../organization-activity.postgres.test.ts | 27 +- .../lib/billing/core/organization-activity.ts | 42 +++ .../usage-analytics-queries.postgres.test.ts | 64 +++- .../billing/core/usage-analytics-queries.ts | 248 ++++++++++++-- .../lib/billing/core/usage-analytics.test.ts | 138 ++++++++ apps/sim/lib/billing/core/usage-analytics.ts | 175 +++++++++- .../billing/core/usage-segment-cache.test.ts | 124 +++++++ .../lib/billing/core/usage-segment-cache.ts | 207 ++++++++++++ .../emcn/src/components/charts/bar-chart.tsx | 274 +++++++++++----- .../components/charts/chart-data-table.tsx | 22 +- .../src/components/charts/chart-format.ts | 20 +- .../components/charts/chart-layout.test.tsx | 157 ++++++--- .../src/components/charts/donut-chart.tsx | 84 ----- packages/emcn/src/components/charts/index.ts | 3 +- 42 files changed, 2689 insertions(+), 757 deletions(-) rename apps/sim/app/api/organizations/[id]/usage/{summary => overview}/route.ts (62%) create mode 100644 apps/sim/ee/organization-usage/components/usage-credits.test.tsx create mode 100644 apps/sim/ee/organization-usage/components/usage-credits.tsx create mode 100644 apps/sim/ee/organization-usage/components/usage-member-avatar.tsx delete mode 100644 apps/sim/ee/organization-usage/components/usage-source-mix.tsx delete mode 100644 apps/sim/ee/organization-usage/components/usage-summary.test.tsx delete mode 100644 apps/sim/ee/organization-usage/components/usage-summary.tsx create mode 100644 apps/sim/ee/organization-usage/components/usage-top-cards.tsx create mode 100644 apps/sim/ee/organization-usage/hooks/use-legend-highlight.ts create mode 100644 apps/sim/lib/billing/application/organization-usage/get-organization-usage-overview.test.ts create mode 100644 apps/sim/lib/billing/application/organization-usage/get-organization-usage-overview.ts create mode 100644 apps/sim/lib/billing/core/organization-activity-summary.ts create mode 100644 apps/sim/lib/billing/core/usage-segment-cache.test.ts create mode 100644 apps/sim/lib/billing/core/usage-segment-cache.ts delete mode 100644 packages/emcn/src/components/charts/donut-chart.tsx diff --git a/apps/sim/app/api/organizations/[id]/usage/summary/route.ts b/apps/sim/app/api/organizations/[id]/usage/overview/route.ts similarity index 62% rename from apps/sim/app/api/organizations/[id]/usage/summary/route.ts rename to apps/sim/app/api/organizations/[id]/usage/overview/route.ts index 4d1111e1962..8294ead619c 100644 --- a/apps/sim/app/api/organizations/[id]/usage/summary/route.ts +++ b/apps/sim/app/api/organizations/[id]/usage/overview/route.ts @@ -1,24 +1,19 @@ -import { getOrganizationUsageSummaryContract } from '@/lib/api/contracts/organization-usage' +import { getOrganizationUsageOverviewContract } from '@/lib/api/contracts/organization-usage' import { defineInternalJsonRoute, internalRateLimits, internalSessionAuth, } from '@/lib/api/server/routes' -import { getOrganizationUsageSummary } from '@/lib/billing/application/organization-usage/get-organization-usage-summary' +import { getOrganizationUsageOverview } from '@/lib/billing/application/organization-usage/get-organization-usage-overview' import { organizationUsageOperations } from '@/lib/billing/application/organization-usage/operations' import { organizationUsageErrorPolicy } from '@/app/api/organizations/[id]/usage/error-policy' export const dynamic = 'force-dynamic' -/** - * Everything above the fold in one round trip. Kept separate from the breakdown - * route because every read here is index-covered, and folding in a dimension that - * heap-scans would put that cost on first paint. - */ export const GET = defineInternalJsonRoute({ - contract: getOrganizationUsageSummaryContract, + contract: getOrganizationUsageOverviewContract, auth: internalSessionAuth, - operation: organizationUsageOperations.readSummary, + operation: organizationUsageOperations.readOverview, rateLimit: internalRateLimits.none({ reason: 'Authenticated org-admin settings read, gated on enterprise entitlement and billing authority', @@ -32,6 +27,6 @@ export const GET = defineInternalJsonRoute({ endDate: query.endDate ? new Date(query.endDate) : undefined, timezone: query.timezone, }), - useCase: getOrganizationUsageSummary, + useCase: getOrganizationUsageOverview, present: (result) => result, }) diff --git a/apps/sim/app/api/v2/organizations/[organizationId]/usage/route.test.ts b/apps/sim/app/api/v2/organizations/[organizationId]/usage/route.test.ts index aa72574f436..0336f0878f4 100644 --- a/apps/sim/app/api/v2/organizations/[organizationId]/usage/route.test.ts +++ b/apps/sim/app/api/v2/organizations/[organizationId]/usage/route.test.ts @@ -57,7 +57,7 @@ vi.mock('@/lib/billing/organizations/member-limits', () => ({ vi.mock('@/lib/billing/core/usage-analytics-queries', () => ({ readUsageTotals: mocks.totals, readUsageTimeSeries: mocks.series, - readUsageBreakdown: mocks.breakdown, + readUsageGroups: mocks.breakdown, readUsageEntityNames: vi.fn().mockResolvedValue(new Map()), })) vi.mock('@/lib/billing/core/usage-log', () => ({ getBillingEntityUsageLogs: mocks.logs })) @@ -431,7 +431,9 @@ describe('organization usage API authorization and bounds', () => { ) const response = await breakdown(request('usage/breakdown?dimension=member'), usageContext) expect(response.status).toBe(413) - expect(mocks.breakdown).toHaveBeenCalledWith(expect.any(Array), 'member', undefined, 10_000) + expect(mocks.breakdown).toHaveBeenCalledWith( + expect.objectContaining({ dimension: 'member', maxRows: 10_000 }) + ) }) }) diff --git a/apps/sim/ee/organization-usage/components/activity-summary.tsx b/apps/sim/ee/organization-usage/components/activity-summary.tsx index 47d42aceed1..bfb0db578e4 100644 --- a/apps/sim/ee/organization-usage/components/activity-summary.tsx +++ b/apps/sim/ee/organization-usage/components/activity-summary.tsx @@ -1,12 +1,45 @@ 'use client' import { useMemo } from 'react' -import { BarChart, ChartFrame, DashboardMetric, DonutChart, formatChartLatency } from '@sim/emcn' +import { + BarChart, + type BarChartSeries, + ChartFrame, + ChartLegend, + type ChartLegendItem, + DashboardMetric, + formatChartLatency, +} from '@sim/emcn' import type { OrganizationActivitySummary } from '@/lib/api/contracts/organization-activity' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { USAGE_CHAT_COLOR } from '@/ee/organization-usage/constants' +import { useLegendHighlight } from '@/ee/organization-usage/hooks/use-legend-highlight' import { useOrganizationActivitySummary } from '@/hooks/queries/organization-activity' import type { OrganizationUsageWindowKey } from '@/hooks/queries/utils/organization-usage-keys' +const CHART_HEIGHT = 180 + +/** + * Outcome layers, bottom-up. Failed is the status red and sits on the stack where a + * spike reads at a glance; Other (cancelled, paused, unfinished) stays neutral. + */ +const OUTCOMES = [ + { id: 'completed', label: 'Completed', color: 'var(--brand-blue)' }, + { id: 'failed', label: 'Failed', color: 'var(--text-error)' }, + { id: 'other', label: 'Other', color: 'var(--text-muted)' }, +] as const + +const OUTCOME_LEGEND: ChartLegendItem[] = [...OUTCOMES] +const OUTCOME_IDS = OUTCOMES.map((outcome) => outcome.id) + +type ActivityPoint = OrganizationActivitySummary['series'][number] + +const OUTCOME_VALUE: Record<(typeof OUTCOMES)[number]['id'], (point: ActivityPoint) => number> = { + completed: (point) => point.completed, + failed: (point) => point.failed, + other: (point) => Math.max(0, point.workflowRuns - point.completed - point.failed), +} + interface ActivitySummaryProps { summary?: OrganizationActivitySummary loading?: boolean @@ -24,30 +57,26 @@ export function formatFailureRate(rate: number | null): string { } export function ActivitySummary({ summary, loading, error, onRetry }: ActivitySummaryProps) { - const workflowSeries = useMemo( + const highlight = useLegendHighlight(OUTCOME_IDS) + + const outcomeSeries = useMemo( () => - summary?.series.map((point) => ({ - timestamp: point.timestamp, - value: point.workflowRuns, - })) ?? [], + OUTCOMES.map((outcome) => ({ + ...outcome, + data: (summary?.series ?? []).map((point) => ({ + timestamp: point.timestamp, + value: OUTCOME_VALUE[outcome.id](point), + })), + })), [summary?.series] ) + const chatSeries = useMemo( () => - summary?.series.map((point) => ({ - timestamp: point.timestamp, - value: point.chatRuns, - })) ?? [], - [summary?.series] - ) - const failureSeries = useMemo( - () => - summary?.series.map((point) => ({ - timestamp: point.timestamp, - value: point.failed, - })) ?? [], + summary?.series.map((point) => ({ timestamp: point.timestamp, value: point.chatRuns })) ?? [], [summary?.series] ) + const totals = summary?.totals const metrics = [ { @@ -81,16 +110,8 @@ export function ActivitySummary({ summary, loading, error, onRetry }: ActivitySu description: 'Completed and failed workflows with a recorded duration.', }, ] - const outcomes = [ - { label: 'Completed', value: totals?.completed ?? 0, color: 'var(--indicator-seat-filled)' }, - { label: 'Failed', value: totals?.failed ?? 0, color: 'var(--text-error)' }, - { - label: 'Other', - value: totals ? totals.workflowRuns - totals.completed - totals.failed : 0, - color: 'var(--text-muted)', - }, - ] const chartState = { loading, error: error ? "Couldn't load activity." : undefined, onRetry } + return (
@@ -104,41 +125,32 @@ export function ActivitySummary({ summary, loading, error, onRetry }: ActivitySu ))}
- +
+ + + + +
+ - - - - - - - - -
) diff --git a/apps/sim/ee/organization-usage/components/usage-consumers.tsx b/apps/sim/ee/organization-usage/components/usage-consumers.tsx index 8a508eaf2c9..aef6f01de4e 100644 --- a/apps/sim/ee/organization-usage/components/usage-consumers.tsx +++ b/apps/sim/ee/organization-usage/components/usage-consumers.tsx @@ -1,6 +1,6 @@ 'use client' -import type { ComponentType } from 'react' +import type { ComponentType, ReactNode } from 'react' import { cn, disclosureChevronClass, formatChartCompactNumber } from '@sim/emcn' import { ArrowRight, ChevronDown } from '@sim/emcn/icons' import { @@ -89,6 +89,8 @@ export const USAGE_PROVIDER_ICON_IDS = Object.keys(PROVIDER_ICONS) interface UsageConsumerRowProps { row: OrganizationUsageBreakdownRow + /** Replaces the provider mark, e.g. with a member's avatar. */ + leading?: ReactNode /** BYOK rows carry no cost, so tokens are the only usage they can show. */ showTokensOnly: boolean onSelect?: (row: OrganizationUsageBreakdownRow) => void @@ -126,6 +128,7 @@ export const USAGE_ROW_CLASSES = 'flex w-full items-center gap-2.5 rounded-lg p- */ function UsageConsumerRow({ row, + leading, showTokensOnly, onSelect, actions, @@ -148,14 +151,15 @@ function UsageConsumerRow({ onSelect && 'transition-colors hover-hover:bg-[var(--surface-active)]' )} > - {ProviderIcon && } + {leading ?? + (ProviderIcon && )} {row.label}