diff --git a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.test.ts b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.test.ts deleted file mode 100644 index 77192d8d77..0000000000 --- a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Tests for Subscription Transfer API - * - * @vitest-environment node - */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockAdminMember, - mockDb, - mockLogger, - mockOrganization, - mockRegularMember, - mockSubscription, - mockUser, -} from '@/app/api/__test-utils__/utils' - -describe('Subscription Transfer API Routes', () => { - beforeEach(() => { - vi.resetModules() - - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: mockUser, - }), - })) - - vi.doMock('@/lib/logs/console/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), - })) - - vi.doMock('@sim/db', () => ({ - db: mockDb, - })) - - mockDb.select.mockReturnValue({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([mockSubscription]), - }) - - mockDb.update.mockReturnValue({ - set: vi.fn().mockReturnThis(), - where: vi.fn().mockResolvedValue([{ affected: 1 }]), - }) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe('POST handler', () => { - it('should successfully transfer a personal subscription to an organization', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { - ...mockUser, - id: 'user-123', - }, - }), - })) - - vi.doMock('@sim/db/schema', () => ({ - subscription: { id: 'id', referenceId: 'referenceId' }, - organization: { id: 'id' }, - member: { userId: 'userId', organizationId: 'organizationId', role: 'role' }, - })) - - const mockSubscriptionWithReferenceId = { - ...mockSubscription, - referenceId: 'user-123', - } - - mockDb.select.mockImplementation(() => { - return { - from: () => ({ - where: () => { - if (mockDb.select.mock.calls.length === 1) { - return Promise.resolve([mockSubscriptionWithReferenceId]) - } - if (mockDb.select.mock.calls.length === 2) { - return Promise.resolve([mockOrganization]) - } - return Promise.resolve([mockAdminMember]) - }, - }), - } - }) - - mockDb.update.mockReturnValue({ - set: () => ({ - where: () => Promise.resolve({ affected: 1 }), - }), - }) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - - const data = await response.json() - - expect(response.status).toBe(200) - expect(data).toHaveProperty('success', true) - expect(data).toHaveProperty('message', 'Subscription transferred successfully') - expect(mockDb.update).toHaveBeenCalled() - }) - - it('should test behavior when subscription not found', async () => { - mockDb.select.mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([]), - }) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(403) - expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user') - }) - - it('should test behavior when organization not found', async () => { - const mockSelectImpl = vi - .fn() - .mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([mockSubscription]), - }) - .mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([]), - }) - - mockDb.select.mockImplementation(mockSelectImpl) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(403) - expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user') - }) - - it('should reject transfer if user is not the subscription owner', async () => { - const differentOwnerSubscription = { - ...mockSubscription, - referenceId: 'different-user-123', - } - - mockDb.select.mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([differentOwnerSubscription]), - }) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(403) - expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user') - expect(mockDb.update).not.toHaveBeenCalled() - }) - - it('should reject non-personal transfer if user is not admin of organization', async () => { - const orgOwnedSubscription = { - ...mockSubscription, - referenceId: 'other-org-789', - } - - const mockSelectImpl = vi - .fn() - .mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([orgOwnedSubscription]), - }) - .mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([mockOrganization]), - }) - .mockReturnValueOnce({ - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockResolvedValue([mockRegularMember]), - }) - - mockDb.select.mockImplementation(mockSelectImpl) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(403) - expect(data).toHaveProperty('error', 'Unauthorized - subscription does not belong to user') - expect(mockDb.update).not.toHaveBeenCalled() - }) - - it('should reject invalid request parameters', async () => { - const req = createMockRequest('POST', {}) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(400) - expect(data).toHaveProperty('error', 'Invalid request parameters') - expect(mockDb.update).not.toHaveBeenCalled() - }) - - it('should handle authentication error', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue(null), - })) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(401) - expect(data).toHaveProperty('error', 'Unauthorized') - expect(mockDb.update).not.toHaveBeenCalled() - }) - - it('should handle internal server error', async () => { - mockDb.select.mockImplementation(() => { - throw new Error('Database error') - }) - - const req = createMockRequest('POST', { - organizationId: 'org-456', - }) - - const { POST } = await import('@/app/api/users/me/subscription/[id]/transfer/route') - - const response = await POST(req, { params: Promise.resolve({ id: 'sub-123' }) }) - const data = await response.json() - - expect(response.status).toBe(500) - expect(data).toHaveProperty('error', 'Failed to transfer subscription') - expect(mockLogger.error).toHaveBeenCalled() - }) - }) -}) diff --git a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts index 3a2986b537..a20f600b7d 100644 --- a/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts +++ b/apps/sim/app/api/users/me/subscription/[id]/transfer/route.ts @@ -81,9 +81,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) .then((rows) => rows[0]) - const isPersonalTransfer = sub.referenceId === session.user.id - - if (!isPersonalTransfer && (!mem || (mem.role !== 'owner' && mem.role !== 'admin'))) { + if (!mem || (mem.role !== 'owner' && mem.role !== 'admin')) { return NextResponse.json( { error: 'Unauthorized - user is not admin of organization' }, { status: 403 } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx index 49c670b251..e3ab22cd78 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans.tsx @@ -135,18 +135,22 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }: .filter(([, v]) => v) .map(([k]) => k) ) - const filterTree = (spans: TraceSpan[]): TraceSpan[] => + const filterTree = (spans: TraceSpan[], parentIsWorkflow = false): TraceSpan[] => spans - .map((s) => ({ ...s })) + .map((s) => normalizeChildWorkflowSpan(s)) .filter((s) => { const tl = s.type?.toLowerCase?.() || '' if (tl === 'workflow') return true + if (parentIsWorkflow) return true return allowed.has(tl) }) - .map((s) => ({ - ...s, - children: s.children ? filterTree(s.children) : undefined, - })) + .map((s) => { + const tl = s.type?.toLowerCase?.() || '' + return { + ...s, + children: s.children ? filterTree(s.children, tl === 'workflow') : undefined, + } + }) return traceSpans ? filterTree(traceSpans) : [] }, [traceSpans, effectiveTypeFilters]) @@ -181,7 +185,6 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }: return () => ro.disconnect() }, []) - // Early return after all hooks are declared to comply with React's Rules of Hooks if (!traceSpans || traceSpans.length === 0) { return
No trace data available
} @@ -217,21 +220,19 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }:
{filtered.map((span, index) => { - const normalizedSpan = normalizeChildWorkflowSpan(span) const hasSubItems = Boolean( - (normalizedSpan.children && normalizedSpan.children.length > 0) || - (normalizedSpan.toolCalls && normalizedSpan.toolCalls.length > 0) || - normalizedSpan.input || - normalizedSpan.output + (span.children && span.children.length > 0) || + (span.toolCalls && span.toolCalls.length > 0) || + span.input || + span.output ) - // Calculate gap from previous span (for sequential execution visualization) let gapMs = 0 let gapPercent = 0 if (index > 0) { const prevSpan = filtered[index - 1] const prevEndTime = new Date(prevSpan.endTime).getTime() - const currentStartTime = new Date(normalizedSpan.startTime).getTime() + const currentStartTime = new Date(span.startTime).getTime() gapMs = currentStartTime - prevEndTime if (gapMs > 0 && actualTotalDuration > 0) { gapPercent = (gapMs / actualTotalDuration) * 100 @@ -241,13 +242,13 @@ export function TraceSpans({ traceSpans, totalDuration = 0, onExpansionChange }: return ( (null) - const [usageExceeded, setUsageExceeded] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [isUpdating, setIsUpdating] = useState(false) - const [error, setError] = useState(null) - - /** - * Check user/organization usage limits with caching - */ - const checkUsage = useCallback( - async (forceRefresh = false): Promise => { - const now = Date.now() - const cacheAge = now - usageDataCache.timestamp - - // Return cached data if still valid and not forcing refresh - if (!forceRefresh && usageDataCache.data && cacheAge < usageDataCache.expirationMs) { - logger.info('Using cached usage data', { - cacheAge: `${Math.round(cacheAge / 1000)}s`, - }) - return usageDataCache.data - } - - setIsLoading(true) - setError(null) - - try { - // Build query params - const params = new URLSearchParams({ context }) - if (context === 'organization' && organizationId) { - params.append('organizationId', organizationId) - } - - // Primary: call server-side usage check to mirror backend enforcement - const res = await fetch(`/api/usage?${params.toString()}`, { cache: 'no-store' }) - if (res.ok) { - const payload = await res.json() - const usage = normalizeUsageData(payload?.data) - - // Update cache - usageDataCache = { - data: usage, - timestamp: now, - expirationMs: usageDataCache.expirationMs, - } - - setUsageData(usage) - setUsageExceeded(usage?.isExceeded || false) - return usage - } - - // No fallback available - React Query handles this globally - throw new Error('Failed to fetch usage data') - } catch (err) { - const error = err instanceof Error ? err : new Error('Failed to check usage limits') - logger.error('Error checking usage limits:', { error }) - setError(error) - return null - } finally { - setIsLoading(false) - } - }, - [context, organizationId] - ) - - /** - * Update usage limit for user or organization - */ - const updateLimit = useCallback( - async (newLimit: number): Promise<{ success: boolean; error?: string }> => { - setIsUpdating(true) - setError(null) - - try { - if (context === 'organization') { - if (!organizationId) { - throw new Error('Organization ID is required') - } - - const response = await fetch('/api/usage', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ context: 'organization', organizationId, limit: newLimit }), - }) - - const data = await response.json() - if (!response.ok) { - throw new Error(data.error || 'Failed to update limit') - } - - // Clear cache and refresh - clearCache() - await checkUsage(true) - - return { success: true } - } - - // User context - use API directly - const response = await fetch('/api/usage', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ context: 'user', limit: newLimit }), - }) - - const data = await response.json() - if (!response.ok) { - throw new Error(data.error || 'Failed to update limit') - } - - // Clear cache and refresh - clearCache() - await checkUsage(true) - - return { success: true } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to update usage limit' - logger.error('Failed to update usage limit', { error: err }) - setError(err instanceof Error ? err : new Error(errorMessage)) - return { success: false, error: errorMessage } - } finally { - setIsUpdating(false) - } - }, - [context, organizationId, checkUsage] - ) - - /** - * Refresh usage data, bypassing cache - */ - const refresh = useCallback(async () => { - return checkUsage(true) - }, [checkUsage]) - - /** - * Clear the cache (useful for testing or forced refresh) - */ - const clearCache = useCallback(() => { - usageDataCache = { - data: null, - timestamp: 0, - expirationMs: usageDataCache.expirationMs, - } - }, []) + // For now, we only support user context via React Query + // Organization context should use useOrganizationBilling directly + const { data: subscriptionData, isLoading } = useSubscriptionData() - /** - * Auto-refresh on mount if enabled - */ - useEffect(() => { - if (autoRefresh) { - checkUsage() - } - }, [autoRefresh, checkUsage]) + const usageExceeded = subscriptionData?.data?.usage?.isExceeded || false return { - usageData, usageExceeded, isLoading, - isUpdating, - error, - checkUsage, - refresh, - updateLimit, - clearCache, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index b0f82b326c..1ff1dd08a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -1,4 +1,5 @@ import { useCallback, useState } from 'react' +import { useQueryClient } from '@tanstack/react-query' import { v4 as uuidv4 } from 'uuid' import { shallow } from 'zustand/shallow' import { createLogger } from '@/lib/logs/console/logger' @@ -11,6 +12,7 @@ import { } from '@/lib/workflows/trigger-utils' import { resolveStartCandidates, StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' +import { subscriptionKeys } from '@/hooks/queries/subscription' import { useExecutionStream } from '@/hooks/use-execution-stream' import { WorkflowValidationError } from '@/serializer' import { useExecutionStore } from '@/stores/execution/store' @@ -84,6 +86,7 @@ function extractExecutionResult(error: unknown): ExecutionResult | null { export function useWorkflowExecution() { const currentWorkflow = useCurrentWorkflow() const { activeWorkflowId, workflows } = useWorkflowRegistry() + const queryClient = useQueryClient() const { toggleConsole, addConsole } = useTerminalConsoleStore() const { getAllVariables } = useEnvironmentStore() const { getVariablesByWorkflowId, variables } = useVariablesStore() @@ -416,7 +419,7 @@ export function useWorkflowExecution() { if (!streamingExecution.stream) return const reader = streamingExecution.stream.getReader() const blockId = (streamingExecution.execution as any)?.blockId - const streamStartTime = Date.now() + let isFirstChunk = true if (blockId) { @@ -577,6 +580,10 @@ export function useWorkflowExecution() { logger.info(`Processed ${processedCount} blocks for streaming tokenization`) } + // Invalidate subscription query to update usage + queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() }) + queryClient.invalidateQueries({ queryKey: subscriptionKeys.usage() }) + const { encodeSSE } = await import('@/lib/utils') controller.enqueue(encodeSSE({ event: 'final', data: result })) // Note: Logs are already persisted server-side via execution-core.ts @@ -639,6 +646,10 @@ export function useWorkflowExecution() { } ;(result.metadata as any).source = 'chat' } + + // Invalidate subscription query to update usage + queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() }) + queryClient.invalidateQueries({ queryKey: subscriptionKeys.usage() }) } return result } catch (error: any) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx index 3f1c159866..2f198c8b2d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/settings-navigation/settings-navigation.tsx @@ -194,7 +194,13 @@ export function SettingsNavigation({ return false } - if (item.requiresTeam && !hasOrganization) { + if (item.requiresTeam) { + const isMember = userRole === 'member' || isAdmin + const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise + + if (isMember) return true + if (isOwner && hasTeamPlan) return true + return false } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx index d33f01f2c4..8539c8a5a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/components/usage-limit/usage-limit.tsx @@ -5,7 +5,7 @@ import { Check, Pencil, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' -import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks' +import { useUpdateOrganizationUsageLimit } from '@/hooks/queries/organization' import { useUpdateUsageLimit } from '@/hooks/queries/subscription' const logger = createLogger('UsageLimit') @@ -41,22 +41,22 @@ export const UsageLimit = forwardRef( const [hasError, setHasError] = useState(false) const [errorType, setErrorType] = useState<'general' | 'belowUsage' | null>(null) const [isEditing, setIsEditing] = useState(false) + const [pendingLimit, setPendingLimit] = useState(null) const inputRef = useRef(null) - const { updateLimit, isUpdating: isOrgUpdating } = useUsageLimits({ - context, - organizationId, - autoRefresh: false, // Don't auto-refresh, we receive values via props - }) + const updateUserLimitMutation = useUpdateUsageLimit() + const updateOrgLimitMutation = useUpdateOrganizationUsageLimit() - const updateUsageLimitMutation = useUpdateUsageLimit() const isUpdating = - context === 'organization' ? isOrgUpdating : updateUsageLimitMutation.isPending + context === 'organization' + ? updateOrgLimitMutation.isPending + : updateUserLimitMutation.isPending const handleStartEdit = () => { if (!canEdit) return setIsEditing(true) - setInputValue(currentLimit.toString()) + const displayLimit = pendingLimit !== null ? pendingLimit : currentLimit + setInputValue(displayLimit.toString()) } useImperativeHandle( @@ -64,12 +64,19 @@ export const UsageLimit = forwardRef( () => ({ startEdit: handleStartEdit, }), - [canEdit, currentLimit] + [canEdit, currentLimit, pendingLimit] ) useEffect(() => { - setInputValue(currentLimit.toString()) - }, [currentLimit]) + if (pendingLimit !== null) { + if (currentLimit === pendingLimit) { + setPendingLimit(null) + setInputValue(currentLimit.toString()) + } + } else { + setInputValue(currentLimit.toString()) + } + }, [currentLimit, pendingLimit]) useEffect(() => { if (isEditing && inputRef.current) { @@ -110,31 +117,19 @@ export const UsageLimit = forwardRef( try { if (context === 'organization') { - const result = await updateLimit(newLimit) - - if (result.success) { - setInputValue(newLimit.toString()) - onLimitUpdated?.(newLimit) - setIsEditing(false) - setErrorType(null) - setHasError(false) - } else { - logger.error('Failed to update usage limit', { error: result.error }) - - if (result.error?.includes('below current usage')) { - setErrorType('belowUsage') - } else { - setErrorType('general') - } - + if (!organizationId) { + logger.error('Organization ID is required for organization context') + setErrorType('general') setHasError(true) + return } - return + await updateOrgLimitMutation.mutateAsync({ organizationId, limit: newLimit }) + } else { + await updateUserLimitMutation.mutateAsync({ limit: newLimit }) } - await updateUsageLimitMutation.mutateAsync({ limit: newLimit }) - + setPendingLimit(newLimit) setInputValue(newLimit.toString()) onLimitUpdated?.(newLimit) setIsEditing(false) @@ -150,13 +145,16 @@ export const UsageLimit = forwardRef( setErrorType('general') } + setPendingLimit(null) + setInputValue(currentLimit.toString()) setHasError(true) } } const handleCancelEdit = () => { setIsEditing(false) - setInputValue(currentLimit.toString()) + const displayLimit = pendingLimit !== null ? pendingLimit : currentLimit + setInputValue(displayLimit.toString()) setHasError(false) setErrorType(null) } @@ -206,7 +204,9 @@ export const UsageLimit = forwardRef( /> ) : ( - ${currentLimit} + + ${pendingLimit !== null ? pendingLimit : currentLimit} + )} {canEdit && (