-
Notifications
You must be signed in to change notification settings - Fork 3.5k
fix(ui): live usage indicator, child trace spans, cancel subscription modal z-index #2044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
2130ba7
cleanup
waleedlatif1 e98a29b
show trace spans for child blocks that error
waleedlatif1 ddf08c9
fix z index for cancel subscription popup
icecrasher321 ecf0067
rotating digit live usage indicator
icecrasher321 8309440
fix
icecrasher321 2e4157c
remove unused code
icecrasher321 52f391a
fix type
icecrasher321 21e3d1d
fix(billing): fix team upgrade
icecrasher321 902bdfd
fix
icecrasher321 d77f825
fix tests
icecrasher321 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 7 additions & 223 deletions
230
...app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/hooks/use-usage-limits.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<UsageData | null>(null) | ||
| const [usageExceeded, setUsageExceeded] = useState(false) | ||
| const [isLoading, setIsLoading] = useState(false) | ||
| const [isUpdating, setIsUpdating] = useState(false) | ||
| const [error, setError] = useState<Error | null>(null) | ||
|
|
||
| /** | ||
| * Check user/organization usage limits with caching | ||
| */ | ||
| const checkUsage = useCallback( | ||
| async (forceRefresh = false): Promise<UsageData | null> => { | ||
| 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 | ||
|
icecrasher321 marked this conversation as resolved.
|
||
|
|
||
| return { | ||
| usageData, | ||
| usageExceeded, | ||
| isLoading, | ||
| isUpdating, | ||
| error, | ||
| checkUsage, | ||
| refresh, | ||
| updateLimit, | ||
| clearCache, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.