Skip to content

Commit 3ba3379

Browse files
authored
fix(settings): update usage data in settings > subs to use reactquery hooks (#1983)
* fix(settings): update usage data in settings > subs to use reactquery hooks * standardize usage pills calculation
1 parent 1e915d5 commit 3ba3379

File tree

6 files changed

+157
-66
lines changed

6 files changed

+157
-66
lines changed

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/shared/usage-header.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
import type { ReactNode } from 'react'
44
import { Badge } from '@/components/emcn'
5+
import { calculateFilledPills, USAGE_PILL_COUNT } from '@/lib/subscription/usage-visualization'
56
import { cn } from '@/lib/utils'
67

78
const GRADIENT_BADGE_STYLES =
89
'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'
910

10-
// Constants matching UsageIndicator
11-
const PILL_COUNT = 8
12-
1311
interface UsageHeaderProps {
1412
title: string
1513
gradientTitle?: boolean
@@ -45,9 +43,9 @@ export function UsageHeader({
4543
}: UsageHeaderProps) {
4644
const progress = progressValue ?? (limit > 0 ? Math.min((current / limit) * 100, 100) : 0)
4745

48-
// Calculate filled pills based on usage percentage
49-
const filledPillsCount = Math.ceil((progress / 100) * PILL_COUNT)
50-
const isAlmostOut = filledPillsCount === PILL_COUNT
46+
// Calculate filled pills based on usage percentage using shared utility (fixed 8 pills)
47+
const filledPillsCount = calculateFilledPills(progress)
48+
const isAlmostOut = filledPillsCount === USAGE_PILL_COUNT
5149

5250
return (
5351
<div className='rounded-[8px] border bg-background p-3 shadow-xs'>
@@ -93,9 +91,9 @@ export function UsageHeader({
9391
</div>
9492
</div>
9593

96-
{/* Pills row - matching UsageIndicator */}
94+
{/* Pills row - fixed 8 pills with shared heuristic */}
9795
<div className='flex items-center gap-[4px]'>
98-
{Array.from({ length: PILL_COUNT }).map((_, i) => {
96+
{Array.from({ length: USAGE_PILL_COUNT }).map((_, i) => {
9997
const isFilled = i < filledPillsCount
10098
return (
10199
<div

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription.tsx

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
getVisiblePlans,
3636
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription-permissions'
3737
import { useOrganizationBilling, useOrganizations } from '@/hooks/queries/organization'
38-
import { useSubscriptionData, useUsageData, useUsageLimitData } from '@/hooks/queries/subscription'
38+
import { useSubscriptionData, useUsageLimitData } from '@/hooks/queries/subscription'
3939
import { useUpdateWorkspaceSettings, useWorkspaceSettings } from '@/hooks/queries/workspace'
4040
import { useGeneralStore } from '@/stores/settings/general/store'
4141

@@ -170,7 +170,6 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
170170

171171
// React Query hooks for data fetching
172172
const { data: subscriptionData, isLoading: isSubscriptionLoading } = useSubscriptionData()
173-
const { data: usageResponse, isLoading: isUsageLoading } = useUsageData()
174173
const { data: usageLimitResponse, isLoading: isUsageLimitLoading } = useUsageLimitData()
175174
const { data: workspaceData, isLoading: isWorkspaceLoading } = useWorkspaceSettings(workspaceId)
176175
const updateWorkspaceMutation = useUpdateWorkspaceSettings()
@@ -188,38 +187,38 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
188187
const usageLimitRef = useRef<UsageLimitRef | null>(null)
189188

190189
// Combine all loading states
191-
const isLoading =
192-
isSubscriptionLoading || isUsageLoading || isUsageLimitLoading || isWorkspaceLoading
190+
const isLoading = isSubscriptionLoading || isUsageLimitLoading || isWorkspaceLoading
193191

194-
// Extract subscription status from data
192+
// Extract subscription status from subscriptionData.data
195193
const subscription = {
196-
isFree: subscriptionData?.plan === 'free' || !subscriptionData?.plan,
197-
isPro: subscriptionData?.plan === 'pro',
198-
isTeam: subscriptionData?.plan === 'team',
199-
isEnterprise: subscriptionData?.plan === 'enterprise',
194+
isFree: subscriptionData?.data?.plan === 'free' || !subscriptionData?.data?.plan,
195+
isPro: subscriptionData?.data?.plan === 'pro',
196+
isTeam: subscriptionData?.data?.plan === 'team',
197+
isEnterprise: subscriptionData?.data?.plan === 'enterprise',
200198
isPaid:
201-
subscriptionData?.plan &&
202-
['pro', 'team', 'enterprise'].includes(subscriptionData.plan) &&
203-
subscriptionData?.status === 'active',
204-
plan: subscriptionData?.plan || 'free',
205-
status: subscriptionData?.status || 'inactive',
206-
seats: subscriptionData?.seats || 1,
199+
subscriptionData?.data?.plan &&
200+
['pro', 'team', 'enterprise'].includes(subscriptionData.data.plan) &&
201+
subscriptionData?.data?.status === 'active',
202+
plan: subscriptionData?.data?.plan || 'free',
203+
status: subscriptionData?.data?.status || 'inactive',
204+
seats: subscriptionData?.data?.seats || 1,
207205
}
208206

209-
// Extract usage data
207+
// Extract usage data from subscriptionData.data.usage (same source as panel usage indicator)
210208
const usage = {
211-
current: usageResponse?.usage?.current || 0,
212-
limit: usageResponse?.usage?.limit || 0,
213-
percentUsed: usageResponse?.usage?.percentUsed || 0,
209+
current: subscriptionData?.data?.usage?.current || 0,
210+
limit: subscriptionData?.data?.usage?.limit || 0,
211+
percentUsed: subscriptionData?.data?.usage?.percentUsed || 0,
214212
}
215213

214+
// Extract usage limit metadata from usageLimitResponse.data
216215
const usageLimitData = {
217-
currentLimit: usageLimitResponse?.usage?.limit || 0,
218-
minimumLimit: usageLimitResponse?.usage?.minimumLimit || (subscription.isPro ? 20 : 40),
216+
currentLimit: usageLimitResponse?.data?.currentLimit || 0,
217+
minimumLimit: usageLimitResponse?.data?.minimumLimit || (subscription.isPro ? 20 : 40),
219218
}
220219

221220
// Extract billing status
222-
const billingStatus = subscriptionData?.billingBlocked ? 'blocked' : 'ok'
221+
const billingStatus = subscriptionData?.data?.billingBlocked ? 'blocked' : 'ok'
223222

224223
// Extract workspace settings
225224
const billedAccountUserId = workspaceData?.settings?.workspace?.billedAccountUserId ?? null
@@ -406,20 +405,18 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
406405
? usage.current // placeholder; rightContent will render UsageLimit
407406
: usage.limit
408407
}
409-
isBlocked={Boolean(subscriptionData?.billingBlocked)}
408+
isBlocked={Boolean(subscriptionData?.data?.billingBlocked)}
410409
status={billingStatus}
411410
percentUsed={
412411
subscription.isEnterprise || subscription.isTeam
413412
? organizationBillingData?.totalUsageLimit &&
414413
organizationBillingData.totalUsageLimit > 0 &&
415414
organizationBillingData.totalCurrentUsage !== undefined
416-
? Math.round(
417-
(organizationBillingData.totalCurrentUsage /
418-
organizationBillingData.totalUsageLimit) *
419-
100
420-
)
421-
: Math.round(usage.percentUsed)
422-
: Math.round(usage.percentUsed)
415+
? (organizationBillingData.totalCurrentUsage /
416+
organizationBillingData.totalUsageLimit) *
417+
100
418+
: usage.percentUsed
419+
: usage.percentUsed
423420
}
424421
onResolvePayment={async () => {
425422
try {
@@ -467,7 +464,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
467464
/>
468465
) : undefined
469466
}
470-
progressValue={Math.min(Math.round(usage.percentUsed), 100)}
467+
progressValue={Math.min(usage.percentUsed, 100)}
471468
/>
472469
</div>
473470

@@ -544,11 +541,11 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
544541
)}
545542

546543
{/* Next Billing Date */}
547-
{subscription.isPaid && subscriptionData?.periodEnd && (
544+
{subscription.isPaid && subscriptionData?.data?.periodEnd && (
548545
<div className='mt-4 flex items-center justify-between'>
549546
<span className='font-medium text-sm'>Next Billing Date</span>
550547
<span className='text-muted-foreground text-sm'>
551-
{new Date(subscriptionData.periodEnd).toLocaleDateString()}
548+
{new Date(subscriptionData.data.periodEnd).toLocaleDateString()}
552549
</span>
553550
</div>
554551
)}
@@ -574,8 +571,8 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
574571
isPaid: subscription.isPaid,
575572
}}
576573
subscriptionData={{
577-
periodEnd: subscriptionData?.periodEnd || null,
578-
cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd,
574+
periodEnd: subscriptionData?.data?.periodEnd || null,
575+
cancelAtPeriodEnd: subscriptionData?.data?.cancelAtPeriodEnd,
579576
}}
580577
/>
581578
</div>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/team-management/components/team-seats-overview/team-seats-overview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function TeamSeatsOverview({
128128
key={i}
129129
className={cn(
130130
'h-[6px] flex-1 rounded-full transition-colors',
131-
isFilled ? 'bg-[#4285F4]' : 'bg-[#2C2C2C]'
131+
isFilled ? 'bg-[#34B5FF]' : 'bg-[#2C2C2C]'
132132
)}
133133
/>
134134
)

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/usage-indicator/usage-indicator.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
4949
const sidebarWidth = useSidebarStore((state) => state.sidebarWidth)
5050

5151
/**
52-
* Calculate pill count based on sidebar width
53-
* Starts at MIN_PILL_COUNT at minimum width, adds 1 pill per WIDTH_PER_PILL increase
52+
* Calculate pill count based on sidebar width (6-8 pills dynamically)
53+
* This provides responsive feedback as the sidebar width changes
5454
*/
5555
const pillCount = useMemo(() => {
5656
const widthDelta = sidebarWidth - MIN_SIDEBAR_WIDTH
@@ -100,6 +100,8 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
100100

101101
/**
102102
* Calculate which pills should be filled based on usage percentage
103+
* Uses shared Math.ceil heuristic but with dynamic pill count (6-8)
104+
* This ensures consistent calculation logic while maintaining responsive pill count
103105
*/
104106
const filledPillsCount = Math.ceil((progressPercentage / 100) * pillCount)
105107
const isAlmostOut = filledPillsCount === pillCount

apps/sim/hooks/queries/subscription.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,32 @@ export function useSubscriptionData() {
3434
}
3535

3636
/**
37-
* Fetch user usage data
37+
* Fetch user usage limit metadata
38+
* Note: This endpoint returns limit information (currentLimit, minimumLimit, canEdit, etc.)
39+
* For actual usage data (current, limit, percentUsed), use useSubscriptionData() instead
3840
*/
39-
async function fetchUsageData() {
41+
async function fetchUsageLimitData() {
4042
const response = await fetch('/api/usage?context=user')
4143
if (!response.ok) {
42-
throw new Error('Failed to fetch usage data')
44+
throw new Error('Failed to fetch usage limit data')
4345
}
4446
return response.json()
4547
}
4648

4749
/**
48-
* Base hook to fetch user usage data (single query)
50+
* Hook to fetch usage limit metadata
51+
* Returns: currentLimit, minimumLimit, canEdit, plan, updatedAt
52+
* Use this for editing usage limits, not for displaying current usage
4953
*/
50-
function useUsageDataBase() {
54+
export function useUsageLimitData() {
5155
return useQuery({
5256
queryKey: subscriptionKeys.usage(),
53-
queryFn: fetchUsageData,
57+
queryFn: fetchUsageLimitData,
5458
staleTime: 30 * 1000,
5559
placeholderData: keepPreviousData,
5660
})
5761
}
5862

59-
/**
60-
* Hook to fetch user usage data
61-
*/
62-
export function useUsageData() {
63-
return useUsageDataBase()
64-
}
65-
66-
/**
67-
* Hook to fetch usage limit data
68-
*/
69-
export function useUsageLimitData() {
70-
return useUsageDataBase()
71-
}
72-
7363
/**
7464
* Update usage limit mutation
7565
*/
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Shared utilities for consistent usage visualization across the application.
3+
*
4+
* This module provides a single source of truth for how usage metrics are
5+
* displayed visually through "pills" or progress indicators.
6+
*/
7+
8+
/**
9+
* Number of pills to display in usage indicators.
10+
*
11+
* Using 8 pills provides:
12+
* - 12.5% granularity per pill
13+
* - Good balance between precision and visual clarity
14+
* - Consistent representation across panel and settings
15+
*/
16+
export const USAGE_PILL_COUNT = 8
17+
18+
/**
19+
* Color values for usage pill states
20+
*/
21+
export const USAGE_PILL_COLORS = {
22+
/** Unfilled pill color (gray) */
23+
UNFILLED: '#414141',
24+
/** Normal filled pill color (blue) */
25+
FILLED: '#34B5FF',
26+
/** Warning/limit reached pill color (red) */
27+
AT_LIMIT: '#ef4444',
28+
} as const
29+
30+
/**
31+
* Calculate the number of filled pills based on usage percentage.
32+
*
33+
* Uses Math.ceil() to ensure even minimal usage (0.01%) shows visual feedback.
34+
* This provides better UX by making it clear that there is some usage, even if small.
35+
*
36+
* @param percentUsed - The usage percentage (0-100). Can be a decimal (e.g., 0.315 for 0.315%)
37+
* @returns Number of pills that should be filled (0 to USAGE_PILL_COUNT)
38+
*
39+
* @example
40+
* calculateFilledPills(0.315) // Returns 1 (shows feedback for 0.315% usage)
41+
* calculateFilledPills(50) // Returns 4 (50% of 8 pills)
42+
* calculateFilledPills(100) // Returns 8 (completely filled)
43+
* calculateFilledPills(150) // Returns 8 (clamped to maximum)
44+
*/
45+
export function calculateFilledPills(percentUsed: number): number {
46+
// Clamp percentage to valid range [0, 100]
47+
const safePercent = Math.min(Math.max(percentUsed, 0), 100)
48+
49+
// Calculate filled pills using ceil to show feedback for any usage
50+
return Math.ceil((safePercent / 100) * USAGE_PILL_COUNT)
51+
}
52+
53+
/**
54+
* Determine if usage has reached the limit (all pills filled).
55+
*
56+
* @param percentUsed - The usage percentage (0-100)
57+
* @returns true if all pills should be filled (at or over limit)
58+
*/
59+
export function isUsageAtLimit(percentUsed: number): boolean {
60+
return calculateFilledPills(percentUsed) >= USAGE_PILL_COUNT
61+
}
62+
63+
/**
64+
* Get the appropriate color for a pill based on its state.
65+
*
66+
* @param isFilled - Whether this pill should be filled
67+
* @param isAtLimit - Whether usage has reached the limit
68+
* @returns Hex color string
69+
*/
70+
export function getPillColor(isFilled: boolean, isAtLimit: boolean): string {
71+
if (!isFilled) return USAGE_PILL_COLORS.UNFILLED
72+
if (isAtLimit) return USAGE_PILL_COLORS.AT_LIMIT
73+
return USAGE_PILL_COLORS.FILLED
74+
}
75+
76+
/**
77+
* Generate an array of pill states for rendering.
78+
*
79+
* @param percentUsed - The usage percentage (0-100)
80+
* @returns Array of pill states with colors
81+
*
82+
* @example
83+
* const pills = generatePillStates(50)
84+
* pills.forEach((pill, index) => (
85+
* <Pill key={index} color={pill.color} filled={pill.filled} />
86+
* ))
87+
*/
88+
export function generatePillStates(percentUsed: number): Array<{
89+
filled: boolean
90+
color: string
91+
index: number
92+
}> {
93+
const filledCount = calculateFilledPills(percentUsed)
94+
const atLimit = isUsageAtLimit(percentUsed)
95+
96+
return Array.from({ length: USAGE_PILL_COUNT }, (_, index) => {
97+
const filled = index < filledCount
98+
return {
99+
filled,
100+
color: getPillColor(filled, atLimit),
101+
index,
102+
}
103+
})
104+
}

0 commit comments

Comments
 (0)