From 2130ba745aecfd8ee472d72eec5802906023d270 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 18 Nov 2025 09:37:29 -0800 Subject: [PATCH 01/10] cleanup --- .../components/trace-spans/trace-spans.tsx | 15 +- .../panel-new/hooks/use-usage-limits.ts | 230 +----------------- .../components/usage-limit/usage-limit.tsx | 68 +++--- .../components/team-usage/team-usage.tsx | 23 +- .../usage-indicator/usage-indicator.tsx | 13 +- apps/sim/hooks/queries/api-keys.ts | 7 +- apps/sim/hooks/queries/copilot-keys.ts | 12 +- apps/sim/hooks/queries/creator-profile.ts | 2 - apps/sim/hooks/queries/custom-tools.ts | 11 - apps/sim/hooks/queries/environment.ts | 6 - apps/sim/hooks/queries/folders.ts | 2 - apps/sim/hooks/queries/general-settings.ts | 8 - apps/sim/hooks/queries/mcp.ts | 9 - apps/sim/hooks/queries/oauth-connections.ts | 17 -- apps/sim/hooks/queries/organization.ts | 98 ++++++-- apps/sim/hooks/queries/sso.ts | 5 - apps/sim/hooks/queries/subscription.ts | 53 +++- apps/sim/hooks/queries/user-profile.ts | 5 - apps/sim/hooks/queries/workspace-files.ts | 7 - apps/sim/hooks/queries/workspace.ts | 1 - 20 files changed, 193 insertions(+), 399 deletions(-) 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 49c670b2514..201e05bbeb6 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,23 @@ 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 })) .filter((s) => { const tl = s.type?.toLowerCase?.() || '' if (tl === 'workflow') return true + // If parent is a workflow span, always include children (child trace spans from workflow blocks) + 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]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks/use-usage-limits.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks/use-usage-limits.ts index a1ba470832d..22624493038 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks/use-usage-limits.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks/use-usage-limits.ts @@ -1,239 +1,23 @@ -import { useCallback, useEffect, useState } from 'react' -import { createLogger } from '@/lib/logs/console/logger' -import type { UsageData as StoreUsageData } from '@/lib/subscription/types' - -const logger = createLogger('useUsageLimits') - -/** - * Extended usage data structure that combines API response and store data. - * Supports both 'current' (from store) and 'currentUsage' (from API) for compatibility. - */ -export interface UsageData { - percentUsed: number - isWarning: boolean - isExceeded: boolean - current: number - currentUsage: number - limit: number -} - -/** - * Normalizes usage data to ensure both 'current' and 'currentUsage' fields exist - */ -function normalizeUsageData(data: StoreUsageData | any): UsageData { - return { - percentUsed: data.percentUsed, - isWarning: data.isWarning, - isExceeded: data.isExceeded, - current: data.current ?? data.currentUsage ?? 0, - currentUsage: data.currentUsage ?? data.current ?? 0, - limit: data.limit, - } -} +import { useSubscriptionData } from '@/hooks/queries/subscription' /** - * Cache for usage data to prevent excessive API calls + * Simplified hook that uses React Query for usage limits. + * Provides usage exceeded status from existing subscription data. */ -let usageDataCache: { - data: UsageData | null - timestamp: number - expirationMs: number -} = { - data: null, - timestamp: 0, - expirationMs: 60 * 1000, // Cache expires after 1 minute -} -/** - * Custom hook to manage usage limits with caching and automatic refresh. - * Provides usage data, exceeded status, and methods to check, refresh, and update limits. - * - * Features: - * - Automatic caching with 60-second expiration - * - Fallback to subscription store if API unavailable - * - Auto-refresh on mount - * - Manual refresh capability - * - Update limit functionality (for user and organization contexts) - * - Integration with usage-limit.tsx component - * - * @param options - Configuration options - * @param options.context - Context for usage check ('user' or 'organization') - * @param options.organizationId - Required when context is 'organization' - * @param options.autoRefresh - Whether to automatically check on mount (default: true) - * @returns Usage state and helper methods - */ export function useUsageLimits(options?: { context?: 'user' | 'organization' organizationId?: string autoRefresh?: boolean }) { - const { context = 'user', organizationId, autoRefresh = true } = options || {} - - const [usageData, setUsageData] = useState(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/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 d33f01f2c4a..8539c8a5a17 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 && (