From dab70a8f1d7d7a4d95d70df3eec5afbd80118aea Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:07:53 -0800 Subject: [PATCH 01/17] fix(logs): show block inputs (#1979) * Fix executor lgos block inputs * Fix Comment --- apps/sim/executor/execution/block-executor.ts | 3 +++ apps/sim/lib/environment.ts | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 618f4822447..a50aa5951c1 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -72,6 +72,9 @@ export class BlockExecutor { try { resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + if (blockLog) { + blockLog.input = resolvedInputs + } } catch (error) { cleanupSelfReference?.() return this.handleBlockError( diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index 32ec4574701..835f54c8bc1 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -1,7 +1,7 @@ /** * Environment utility functions for consistent environment detection across the application */ -import { env, isTruthy } from './env' +import { env, getEnv, isTruthy } from './env' /** * Is the application running in production mode @@ -21,7 +21,9 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = true +export const isHosted = + getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' || + getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled From 785f847c4825a7e8c842524557b961c5fdd8be91 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 13 Nov 2025 19:24:33 -0800 Subject: [PATCH 02/17] fix(onedrive): parse array values correctly (#1981) * fix(onedrive): parse array values correctly * fix onedrive * fix * fix onedrive input parsing by reusing code subblock * fix type --- .../app/api/tools/onedrive/upload/route.ts | 16 ++++-- .../sub-block/components/code/code.tsx | 42 ++++++++++++---- .../editor/components/sub-block/sub-block.tsx | 2 +- apps/sim/blocks/blocks/onedrive.ts | 27 ++++++---- apps/sim/blocks/types.ts | 2 +- apps/sim/tools/onedrive/types.ts | 4 +- apps/sim/tools/onedrive/utils.ts | 49 +++++++++++++++++++ 7 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 apps/sim/tools/onedrive/utils.ts diff --git a/apps/sim/app/api/tools/onedrive/upload/route.ts b/apps/sim/app/api/tools/onedrive/upload/route.ts index f1276e3ad43..1f4a0f9f49a 100644 --- a/apps/sim/app/api/tools/onedrive/upload/route.ts +++ b/apps/sim/app/api/tools/onedrive/upload/route.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { generateRequestId } from '@/lib/utils' +import { normalizeExcelValues } from '@/tools/onedrive/utils' export const dynamic = 'force-dynamic' @@ -13,6 +14,14 @@ const logger = createLogger('OneDriveUploadAPI') const MICROSOFT_GRAPH_BASE = 'https://graph.microsoft.com/v1.0' +const ExcelCellSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]) +const ExcelRowSchema = z.array(ExcelCellSchema) +const ExcelValuesSchema = z.union([ + z.string(), + z.array(ExcelRowSchema), + z.array(z.record(ExcelCellSchema)), +]) + const OneDriveUploadSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), fileName: z.string().min(1, 'File name is required'), @@ -20,7 +29,7 @@ const OneDriveUploadSchema = z.object({ folderId: z.string().optional().nullable(), mimeType: z.string().optional(), // Optional Excel write-after-create inputs - values: z.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))).optional(), + values: ExcelValuesSchema.optional(), }) export async function POST(request: NextRequest) { @@ -46,6 +55,7 @@ export async function POST(request: NextRequest) { const body = await request.json() const validatedData = OneDriveUploadSchema.parse(body) + const excelValues = normalizeExcelValues(validatedData.values) let fileBuffer: Buffer let mimeType: string @@ -180,7 +190,7 @@ export async function POST(request: NextRequest) { // If this is an Excel creation and values were provided, write them using the Excel API let excelWriteResult: any | undefined const shouldWriteExcelContent = - isExcelCreation && Array.isArray(validatedData.values) && validatedData.values.length > 0 + isExcelCreation && Array.isArray(excelValues) && excelValues.length > 0 if (shouldWriteExcelContent) { try { @@ -232,7 +242,7 @@ export async function POST(request: NextRequest) { logger.warn(`[${requestId}] Error listing worksheets, using default Sheet1`, listError) } - let processedValues: any = validatedData.values || [] + let processedValues: any = excelValues || [] if ( Array.isArray(processedValues) && diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx index 02092f9eb8a..8fec01358a4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx @@ -221,17 +221,26 @@ export function Code({ // Derived state const effectiveLanguage = (languageValue as 'javascript' | 'python' | 'json') || language + const trimmedCode = code.trim() + const containsReferencePlaceholders = + trimmedCode.includes('{{') || + trimmedCode.includes('}}') || + trimmedCode.includes('<') || + trimmedCode.includes('>') + + const shouldValidateJson = effectiveLanguage === 'json' && !containsReferencePlaceholders + const isValidJson = useMemo(() => { - if (subBlockId !== 'responseFormat' || !code.trim()) { + if (!shouldValidateJson || !trimmedCode) { return true } try { - JSON.parse(code) + JSON.parse(trimmedCode) return true } catch { return false } - }, [subBlockId, code]) + }, [shouldValidateJson, trimmedCode]) const gutterWidthPx = useMemo(() => { const lineCount = code.split('\n').length @@ -309,14 +318,29 @@ export function Code({ : storeValue // Effects: JSON validation + const lastValidationStatus = useRef(true) + useEffect(() => { - if (onValidationChange && subBlockId === 'responseFormat') { - const timeoutId = setTimeout(() => { - onValidationChange(isValidJson) - }, 150) - return () => clearTimeout(timeoutId) + if (!onValidationChange) return + + const nextStatus = shouldValidateJson ? isValidJson : true + if (lastValidationStatus.current === nextStatus) { + return } - }, [isValidJson, onValidationChange, subBlockId]) + + lastValidationStatus.current = nextStatus + + if (!shouldValidateJson) { + onValidationChange(nextStatus) + return + } + + const timeoutId = setTimeout(() => { + onValidationChange(nextStatus) + }, 150) + + return () => clearTimeout(timeoutId) + }, [isValidJson, onValidationChange, shouldValidateJson]) // Effects: AI stream handlers setup useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx index 8328eb7be9d..03c03b9335e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/sub-block.tsx @@ -190,7 +190,7 @@ const renderLabel = (
{config.title} {required && *} - {config.id === 'responseFormat' && ( + {config.type === 'code' && config.language === 'json' && ( = { { id: 'values', title: 'Values', - type: 'long-input', - placeholder: - 'Enter values as JSON array of arrays (e.g., [["A1","B1"],["A2","B2"]]) or an array of objects', + type: 'code', + language: 'json', + generationType: 'json-object', + placeholder: 'Enter a JSON array of rows (e.g., [["A1","B1"],["A2","B2"]])', condition: { field: 'operation', value: 'create_file', @@ -89,6 +91,13 @@ export const OneDriveBlock: BlockConfig = { value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of arrays that can be written directly into an Excel worksheet.', + placeholder: 'Describe the table you want to generate...', + generationType: 'json-object', + }, required: false, }, // File upload (basic mode) @@ -351,17 +360,15 @@ export const OneDriveBlock: BlockConfig = { params: (params) => { const { credential, folderId, fileId, mimeType, values, downloadFileName, ...rest } = params - let parsedValues - try { - parsedValues = values ? JSON.parse(values as string) : undefined - } catch (error) { - throw new Error('Invalid JSON format for values') + let normalizedValues: ReturnType + if (values !== undefined) { + normalizedValues = normalizeExcelValuesForToolParams(values) } return { credential, ...rest, - values: parsedValues, + values: normalizedValues, folderId: folderId || undefined, fileId: fileId || undefined, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, @@ -380,7 +387,7 @@ export const OneDriveBlock: BlockConfig = { fileReference: { type: 'json', description: 'File reference from previous block' }, content: { type: 'string', description: 'Text content to upload' }, mimeType: { type: 'string', description: 'MIME type of file to create' }, - values: { type: 'string', description: 'Cell values for new Excel as JSON' }, + values: { type: 'json', description: 'Cell values for new Excel as JSON' }, fileId: { type: 'string', description: 'File ID to download' }, downloadFileName: { type: 'string', description: 'File name override for download' }, folderId: { type: 'string', description: 'Folder ID' }, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 568c32100a7..ac2c65bad61 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -210,7 +210,7 @@ export interface SubBlockConfig { } }) // Props specific to 'code' sub-block type - language?: 'javascript' | 'json' + language?: 'javascript' | 'json' | 'python' generationType?: GenerationType collapsible?: boolean // Whether the code block can be collapsed defaultCollapsed?: boolean // Whether the code block is collapsed by default diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index cd20b6e2f07..6ceb005c5e3 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -99,7 +99,9 @@ export interface OneDriveToolParams { pageToken?: string exportMimeType?: string // Optional Excel write parameters (used when creating an .xlsx without file content) - values?: (string | number | boolean | null)[][] + values?: + | (string | number | boolean | null)[][] + | Array> } export type OneDriveResponse = diff --git a/apps/sim/tools/onedrive/utils.ts b/apps/sim/tools/onedrive/utils.ts new file mode 100644 index 00000000000..e1b595f944b --- /dev/null +++ b/apps/sim/tools/onedrive/utils.ts @@ -0,0 +1,49 @@ +import type { OneDriveToolParams } from '@/tools/onedrive/types' + +export type ExcelCell = string | number | boolean | null +export type ExcelArrayValues = ExcelCell[][] +export type ExcelObjectValues = Array> +export type NormalizedExcelValues = ExcelArrayValues | ExcelObjectValues + +/** + * Ensures Excel values are always represented as arrays before hitting downstream tooling. + * Accepts JSON strings, array-of-arrays, or array-of-objects and normalizes them. + */ +export function normalizeExcelValues(values: unknown): NormalizedExcelValues | undefined { + if (values === null || values === undefined) { + return undefined + } + + if (typeof values === 'string') { + const trimmed = values.trim() + if (!trimmed) { + return undefined + } + + try { + const parsed = JSON.parse(trimmed) + if (!Array.isArray(parsed)) { + throw new Error('Excel values must be an array of rows or array of objects') + } + return parsed as NormalizedExcelValues + } catch (_error) { + throw new Error('Invalid JSON format for values') + } + } + + if (Array.isArray(values)) { + return values as NormalizedExcelValues + } + + throw new Error('Excel values must be an array of rows or array of objects') +} + +/** + * Convenience helper for contexts that expect the narrower ToolParams typing. + */ +export function normalizeExcelValuesForToolParams( + values: unknown +): OneDriveToolParams['values'] | undefined { + const normalized = normalizeExcelValues(values) + return normalized as OneDriveToolParams['values'] | undefined +} From d86198ad5d51e2db3cf23f21287e3dc89cd80574 Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 13 Nov 2025 19:32:23 -0800 Subject: [PATCH 03/17] feat(files): add presigned URL generation support for execution files (#1980) --- .../contexts/execution/execution-file-manager.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts index bf61be0a762..22ca9ce5481 100644 --- a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts @@ -1,5 +1,4 @@ import { createLogger } from '@/lib/logs/console/logger' -import { getBaseUrl } from '@/lib/urls/utils' import { isUserFile } from '@/lib/utils' import type { UserFile } from '@/executor/types' import type { ExecutionContext } from './execution-file-helpers' @@ -90,7 +89,9 @@ export async function uploadExecutionFile( } try { - const { uploadFile } = await import('@/lib/uploads/core/storage-service') + const { uploadFile, generatePresignedDownloadUrl } = await import( + '@/lib/uploads/core/storage-service' + ) const fileInfo = await uploadFile({ file: fileBuffer, fileName: storageKey, @@ -101,16 +102,15 @@ export async function uploadExecutionFile( metadata, // Pass metadata for cloud storage and database tracking }) - // Generate full URL for file access (useful for passing to external services) - const baseUrl = getBaseUrl() - const fullUrl = `${baseUrl}/api/files/serve/${fileInfo.key}` + // Generate presigned URL for file access (10 minutes expiration) + const fullUrl = await generatePresignedDownloadUrl(fileInfo.key, 'execution', 600) const userFile: UserFile = { id: fileId, name: fileName, size: fileBuffer.length, type: contentType, - url: fullUrl, // Full URL for external access and downstream workflow usage + url: fullUrl, // Presigned URL for external access and downstream workflow usage key: fileInfo.key, context: 'execution', // Preserve context in file object } From 1e915d5427bbd060813a5d44688c15b957bb8149 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 13 Nov 2025 19:33:15 -0800 Subject: [PATCH 04/17] fix(popovers): billed account + async example command (#1982) --- .../components/example-command/example-command.tsx | 2 +- .../settings-modal/components/subscription/subscription.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx index abb39c34258..642e76ad43e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx @@ -196,7 +196,7 @@ export function ExampleCommand({ - + setExampleType('execute')} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription.tsx index 9e8b8137e9c..98a88beed92 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/subscription/subscription.tsx @@ -607,7 +607,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) { - + Workspace admins From 3ba33791f72e6da4606efc2facab7910ee7c56de Mon Sep 17 00:00:00 2001 From: Waleed Date: Thu, 13 Nov 2025 19:57:48 -0800 Subject: [PATCH 05/17] 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 --- .../components/shared/usage-header.tsx | 14 +-- .../components/subscription/subscription.tsx | 67 ++++++----- .../team-seats-overview.tsx | 2 +- .../usage-indicator/usage-indicator.tsx | 6 +- apps/sim/hooks/queries/subscription.ts | 30 ++--- .../lib/subscription/usage-visualization.ts | 104 ++++++++++++++++++ 6 files changed, 157 insertions(+), 66 deletions(-) create mode 100644 apps/sim/lib/subscription/usage-visualization.ts 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 5d1c90cf1ce..a7275686e4e 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 (
@@ -93,9 +91,9 @@ export function UsageHeader({
- {/* Pills row - matching UsageIndicator */} + {/* Pills row - fixed 8 pills with shared heuristic */}
- {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 099a0ee23e1..08a13ed2442 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 7f223aa3da4..f27bc836621 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 524e4b8f8e4..f8e2065f4b8 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 00000000000..e165a1d0f1b --- /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) => ( + * + * )) + */ +export function generatePillStates(percentUsed: number): Array<{ + filled: boolean + color: string + index: number +}> { + const filledCount = calculateFilledPills(percentUsed) + const atLimit = isUsageAtLimit(percentUsed) + + return Array.from({ length: USAGE_PILL_COUNT }, (_, index) => { + const filled = index < filledCount + return { + filled, + color: getPillColor(filled, atLimit), + index, + } + }) +} From 948b6575dc31bc50561740029dab908c06c8ae28 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 13 Nov 2025 20:13:15 -0800 Subject: [PATCH 06/17] fix(output-selector): z-index in chat deploy modal (#1984) --- .../components/chat/components/output-select/output-select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index cfee42d06d2..1ca98e19b93 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -288,7 +288,7 @@ export function OutputSelect({ Date: Fri, 14 Nov 2025 01:14:20 -0800 Subject: [PATCH 07/17] improvement(logs): improved logs search (#1985) * improvement(logs): improved logs search * more * ack PR comments --- apps/sim/app/api/logs/export/route.ts | 7 +- apps/sim/app/api/logs/route.ts | 9 +- .../logs/components/dashboard/controls.tsx | 50 +- .../logs/components/dashboard/kpis.tsx | 8 +- .../logs/components/dashboard/line-chart.tsx | 90 ++- .../components/dashboard/workflow-details.tsx | 220 +++++-- .../components/dashboard/workflows-list.tsx | 4 +- .../logs/components/search/search.tsx | 536 +++++++-------- .../[workspaceId]/logs/dashboard.tsx | 104 ++- .../logs/hooks/use-autocomplete.ts | 423 ------------ .../logs/hooks/use-search-state.ts | 291 ++++++++ .../app/workspace/[workspaceId]/logs/logs.tsx | 16 +- .../[workspaceId]/logs/types/search.ts | 30 + .../search-modal/search-modal.tsx | 2 +- apps/sim/lib/logs/query-parser.ts | 4 +- apps/sim/lib/logs/search-suggestions.test.ts | 182 ----- apps/sim/lib/logs/search-suggestions.ts | 621 ++++++++---------- 17 files changed, 1237 insertions(+), 1360 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/logs/hooks/use-autocomplete.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/hooks/use-search-state.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/types/search.ts delete mode 100644 apps/sim/lib/logs/search-suggestions.test.ts diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index 645d8613333..08cad297a0b 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -60,7 +60,12 @@ export async function GET(request: NextRequest) { let conditions: SQL | undefined = eq(workflow.workspaceId, params.workspaceId) if (params.level && params.level !== 'all') { - conditions = and(conditions, eq(workflowExecutionLogs.level, params.level)) + const levels = params.level.split(',').filter(Boolean) + if (levels.length === 1) { + conditions = and(conditions, eq(workflowExecutionLogs.level, levels[0])) + } else if (levels.length > 1) { + conditions = and(conditions, inArray(workflowExecutionLogs.level, levels)) + } } if (params.workflowIds) { diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index b55e2d1323d..3d7e532825c 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -126,9 +126,14 @@ export async function GET(request: NextRequest) { // Build additional conditions for the query let conditions: SQL | undefined - // Filter by level + // Filter by level (supports comma-separated for OR conditions) if (params.level && params.level !== 'all') { - conditions = and(conditions, eq(workflowExecutionLogs.level, params.level)) + const levels = params.level.split(',').filter(Boolean) + if (levels.length === 1) { + conditions = and(conditions, eq(workflowExecutionLogs.level, levels[0])) + } else if (levels.length > 1) { + conditions = and(conditions, inArray(workflowExecutionLogs.level, levels)) + } } // Filter by specific workflow IDs diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/controls.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/controls.tsx index 485d4cb750c..93bfee6b599 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/controls.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { Loader2, RefreshCw, Search } from 'lucide-react' +import { ArrowUp, Loader2, RefreshCw, Search } from 'lucide-react' import { Button, Tooltip } from '@/components/emcn' import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' @@ -16,7 +16,6 @@ export function Controls({ viewMode, setViewMode, searchComponent, - showExport = true, onExport, }: { searchQuery?: string @@ -72,6 +71,23 @@ export function Controls({ )}
+ {viewMode !== 'dashboard' && ( + + + + + Export CSV + + )} + @@ -91,32 +107,6 @@ export function Controls({ {isRefetching ? 'Refreshing...' : 'Refresh'} - - - - - Export CSV - -
- ) - })} -
- )} -
- {currentHoverDate ? ( -
{currentHoverDate}
- ) : null} + }} + onClick={() => setActiveSeriesId((prev) => (prev === s.id ? null : s.id || null))} + > +