diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/shared/usage-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/shared/usage-header.tsx
index 5d1c90cf1c..a7275686e4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/shared/usage-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/shared/usage-header.tsx
@@ -2,14 +2,12 @@
import type { ReactNode } from 'react'
import { Badge } from '@/components/emcn'
+import { calculateFilledPills, USAGE_PILL_COUNT } from '@/lib/subscription/usage-visualization'
import { cn } from '@/lib/utils'
const GRADIENT_BADGE_STYLES =
'gradient-text h-[1.125rem] rounded-[6px] border-gradient-primary/20 bg-gradient-to-b from-gradient-primary via-gradient-secondary to-gradient-primary px-2 py-0 font-medium text-xs cursor-pointer'
-// Constants matching UsageIndicator
-const PILL_COUNT = 8
-
interface UsageHeaderProps {
title: string
gradientTitle?: boolean
@@ -45,9 +43,9 @@ export function UsageHeader({
}: UsageHeaderProps) {
const progress = progressValue ?? (limit > 0 ? Math.min((current / limit) * 100, 100) : 0)
- // Calculate filled pills based on usage percentage
- const filledPillsCount = Math.ceil((progress / 100) * PILL_COUNT)
- const isAlmostOut = filledPillsCount === PILL_COUNT
+ // Calculate filled pills based on usage percentage using shared utility (fixed 8 pills)
+ const filledPillsCount = calculateFilledPills(progress)
+ const isAlmostOut = filledPillsCount === USAGE_PILL_COUNT
return (
- {Array.from({ length: PILL_COUNT }).map((_, i) => {
+ {Array.from({ length: USAGE_PILL_COUNT }).map((_, i) => {
const isFilled = i < filledPillsCount
return (
(null)
// Combine all loading states
- const isLoading =
- isSubscriptionLoading || isUsageLoading || isUsageLimitLoading || isWorkspaceLoading
+ const isLoading = isSubscriptionLoading || isUsageLimitLoading || isWorkspaceLoading
- // Extract subscription status from data
+ // Extract subscription status from subscriptionData.data
const subscription = {
- isFree: subscriptionData?.plan === 'free' || !subscriptionData?.plan,
- isPro: subscriptionData?.plan === 'pro',
- isTeam: subscriptionData?.plan === 'team',
- isEnterprise: subscriptionData?.plan === 'enterprise',
+ isFree: subscriptionData?.data?.plan === 'free' || !subscriptionData?.data?.plan,
+ isPro: subscriptionData?.data?.plan === 'pro',
+ isTeam: subscriptionData?.data?.plan === 'team',
+ isEnterprise: subscriptionData?.data?.plan === 'enterprise',
isPaid:
- subscriptionData?.plan &&
- ['pro', 'team', 'enterprise'].includes(subscriptionData.plan) &&
- subscriptionData?.status === 'active',
- plan: subscriptionData?.plan || 'free',
- status: subscriptionData?.status || 'inactive',
- seats: subscriptionData?.seats || 1,
+ subscriptionData?.data?.plan &&
+ ['pro', 'team', 'enterprise'].includes(subscriptionData.data.plan) &&
+ subscriptionData?.data?.status === 'active',
+ plan: subscriptionData?.data?.plan || 'free',
+ status: subscriptionData?.data?.status || 'inactive',
+ seats: subscriptionData?.data?.seats || 1,
}
- // Extract usage data
+ // Extract usage data from subscriptionData.data.usage (same source as panel usage indicator)
const usage = {
- current: usageResponse?.usage?.current || 0,
- limit: usageResponse?.usage?.limit || 0,
- percentUsed: usageResponse?.usage?.percentUsed || 0,
+ current: subscriptionData?.data?.usage?.current || 0,
+ limit: subscriptionData?.data?.usage?.limit || 0,
+ percentUsed: subscriptionData?.data?.usage?.percentUsed || 0,
}
+ // Extract usage limit metadata from usageLimitResponse.data
const usageLimitData = {
- currentLimit: usageLimitResponse?.usage?.limit || 0,
- minimumLimit: usageLimitResponse?.usage?.minimumLimit || (subscription.isPro ? 20 : 40),
+ currentLimit: usageLimitResponse?.data?.currentLimit || 0,
+ minimumLimit: usageLimitResponse?.data?.minimumLimit || (subscription.isPro ? 20 : 40),
}
// Extract billing status
- const billingStatus = subscriptionData?.billingBlocked ? 'blocked' : 'ok'
+ const billingStatus = subscriptionData?.data?.billingBlocked ? 'blocked' : 'ok'
// Extract workspace settings
const billedAccountUserId = workspaceData?.settings?.workspace?.billedAccountUserId ?? null
@@ -406,20 +405,18 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
? usage.current // placeholder; rightContent will render UsageLimit
: usage.limit
}
- isBlocked={Boolean(subscriptionData?.billingBlocked)}
+ isBlocked={Boolean(subscriptionData?.data?.billingBlocked)}
status={billingStatus}
percentUsed={
subscription.isEnterprise || subscription.isTeam
? organizationBillingData?.totalUsageLimit &&
organizationBillingData.totalUsageLimit > 0 &&
organizationBillingData.totalCurrentUsage !== undefined
- ? Math.round(
- (organizationBillingData.totalCurrentUsage /
- organizationBillingData.totalUsageLimit) *
- 100
- )
- : Math.round(usage.percentUsed)
- : Math.round(usage.percentUsed)
+ ? (organizationBillingData.totalCurrentUsage /
+ organizationBillingData.totalUsageLimit) *
+ 100
+ : usage.percentUsed
+ : usage.percentUsed
}
onResolvePayment={async () => {
try {
@@ -467,7 +464,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
/>
) : undefined
}
- progressValue={Math.min(Math.round(usage.percentUsed), 100)}
+ progressValue={Math.min(usage.percentUsed, 100)}
/>
@@ -544,11 +541,11 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
)}
{/* Next Billing Date */}
- {subscription.isPaid && subscriptionData?.periodEnd && (
+ {subscription.isPaid && subscriptionData?.data?.periodEnd && (
Next Billing Date
- {new Date(subscriptionData.periodEnd).toLocaleDateString()}
+ {new Date(subscriptionData.data.periodEnd).toLocaleDateString()}
)}
@@ -574,8 +571,8 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
isPaid: subscription.isPaid,
}}
subscriptionData={{
- periodEnd: subscriptionData?.periodEnd || null,
- cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd,
+ periodEnd: subscriptionData?.data?.periodEnd || null,
+ cancelAtPeriodEnd: subscriptionData?.data?.cancelAtPeriodEnd,
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx
index 099a0ee23e..08a13ed244 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx
@@ -128,7 +128,7 @@ export function TeamSeatsOverview({
key={i}
className={cn(
'h-[6px] flex-1 rounded-full transition-colors',
- isFilled ? 'bg-[#4285F4]' : 'bg-[#2C2C2C]'
+ isFilled ? 'bg-[#34B5FF]' : 'bg-[#2C2C2C]'
)}
/>
)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx
index 7f223aa3da..f27bc83662 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx
@@ -49,8 +49,8 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
const sidebarWidth = useSidebarStore((state) => state.sidebarWidth)
/**
- * Calculate pill count based on sidebar width
- * Starts at MIN_PILL_COUNT at minimum width, adds 1 pill per WIDTH_PER_PILL increase
+ * Calculate pill count based on sidebar width (6-8 pills dynamically)
+ * This provides responsive feedback as the sidebar width changes
*/
const pillCount = useMemo(() => {
const widthDelta = sidebarWidth - MIN_SIDEBAR_WIDTH
@@ -100,6 +100,8 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
/**
* Calculate which pills should be filled based on usage percentage
+ * Uses shared Math.ceil heuristic but with dynamic pill count (6-8)
+ * This ensures consistent calculation logic while maintaining responsive pill count
*/
const filledPillsCount = Math.ceil((progressPercentage / 100) * pillCount)
const isAlmostOut = filledPillsCount === pillCount
diff --git a/apps/sim/hooks/queries/subscription.ts b/apps/sim/hooks/queries/subscription.ts
index 524e4b8f8e..f8e2065f4b 100644
--- a/apps/sim/hooks/queries/subscription.ts
+++ b/apps/sim/hooks/queries/subscription.ts
@@ -34,42 +34,32 @@ export function useSubscriptionData() {
}
/**
- * Fetch user usage data
+ * Fetch user usage limit metadata
+ * Note: This endpoint returns limit information (currentLimit, minimumLimit, canEdit, etc.)
+ * For actual usage data (current, limit, percentUsed), use useSubscriptionData() instead
*/
-async function fetchUsageData() {
+async function fetchUsageLimitData() {
const response = await fetch('/api/usage?context=user')
if (!response.ok) {
- throw new Error('Failed to fetch usage data')
+ throw new Error('Failed to fetch usage limit data')
}
return response.json()
}
/**
- * Base hook to fetch user usage data (single query)
+ * Hook to fetch usage limit metadata
+ * Returns: currentLimit, minimumLimit, canEdit, plan, updatedAt
+ * Use this for editing usage limits, not for displaying current usage
*/
-function useUsageDataBase() {
+export function useUsageLimitData() {
return useQuery({
queryKey: subscriptionKeys.usage(),
- queryFn: fetchUsageData,
+ queryFn: fetchUsageLimitData,
staleTime: 30 * 1000,
placeholderData: keepPreviousData,
})
}
-/**
- * Hook to fetch user usage data
- */
-export function useUsageData() {
- return useUsageDataBase()
-}
-
-/**
- * Hook to fetch usage limit data
- */
-export function useUsageLimitData() {
- return useUsageDataBase()
-}
-
/**
* Update usage limit mutation
*/
diff --git a/apps/sim/lib/subscription/usage-visualization.ts b/apps/sim/lib/subscription/usage-visualization.ts
new file mode 100644
index 0000000000..e165a1d0f1
--- /dev/null
+++ b/apps/sim/lib/subscription/usage-visualization.ts
@@ -0,0 +1,104 @@
+/**
+ * Shared utilities for consistent usage visualization across the application.
+ *
+ * This module provides a single source of truth for how usage metrics are
+ * displayed visually through "pills" or progress indicators.
+ */
+
+/**
+ * Number of pills to display in usage indicators.
+ *
+ * Using 8 pills provides:
+ * - 12.5% granularity per pill
+ * - Good balance between precision and visual clarity
+ * - Consistent representation across panel and settings
+ */
+export const USAGE_PILL_COUNT = 8
+
+/**
+ * Color values for usage pill states
+ */
+export const USAGE_PILL_COLORS = {
+ /** Unfilled pill color (gray) */
+ UNFILLED: '#414141',
+ /** Normal filled pill color (blue) */
+ FILLED: '#34B5FF',
+ /** Warning/limit reached pill color (red) */
+ AT_LIMIT: '#ef4444',
+} as const
+
+/**
+ * Calculate the number of filled pills based on usage percentage.
+ *
+ * Uses Math.ceil() to ensure even minimal usage (0.01%) shows visual feedback.
+ * This provides better UX by making it clear that there is some usage, even if small.
+ *
+ * @param percentUsed - The usage percentage (0-100). Can be a decimal (e.g., 0.315 for 0.315%)
+ * @returns Number of pills that should be filled (0 to USAGE_PILL_COUNT)
+ *
+ * @example
+ * calculateFilledPills(0.315) // Returns 1 (shows feedback for 0.315% usage)
+ * calculateFilledPills(50) // Returns 4 (50% of 8 pills)
+ * calculateFilledPills(100) // Returns 8 (completely filled)
+ * calculateFilledPills(150) // Returns 8 (clamped to maximum)
+ */
+export function calculateFilledPills(percentUsed: number): number {
+ // Clamp percentage to valid range [0, 100]
+ const safePercent = Math.min(Math.max(percentUsed, 0), 100)
+
+ // Calculate filled pills using ceil to show feedback for any usage
+ return Math.ceil((safePercent / 100) * USAGE_PILL_COUNT)
+}
+
+/**
+ * Determine if usage has reached the limit (all pills filled).
+ *
+ * @param percentUsed - The usage percentage (0-100)
+ * @returns true if all pills should be filled (at or over limit)
+ */
+export function isUsageAtLimit(percentUsed: number): boolean {
+ return calculateFilledPills(percentUsed) >= USAGE_PILL_COUNT
+}
+
+/**
+ * Get the appropriate color for a pill based on its state.
+ *
+ * @param isFilled - Whether this pill should be filled
+ * @param isAtLimit - Whether usage has reached the limit
+ * @returns Hex color string
+ */
+export function getPillColor(isFilled: boolean, isAtLimit: boolean): string {
+ if (!isFilled) return USAGE_PILL_COLORS.UNFILLED
+ if (isAtLimit) return USAGE_PILL_COLORS.AT_LIMIT
+ return USAGE_PILL_COLORS.FILLED
+}
+
+/**
+ * Generate an array of pill states for rendering.
+ *
+ * @param percentUsed - The usage percentage (0-100)
+ * @returns Array of pill states with colors
+ *
+ * @example
+ * const pills = generatePillStates(50)
+ * pills.forEach((pill, index) => (
+ *