diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 326a18a6f00..fba433b54f9 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata, Viewport } from 'next' import Script from 'next/script' import { PublicEnvScript } from 'next-runtime-env' +import { NuqsAdapter } from 'nuqs/adapters/next/app' import { BrandedLayout } from '@/components/branded-layout' import { PostHogProvider } from '@/app/_shell/providers/posthog-provider' import { generateBrandedMetadata, generateThemeCSS } from '@/ee/whitelabeling' @@ -258,11 +259,13 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= - - - {children} - - + + + + {children} + + + diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index addb5e8932b..8f4e2fb0136 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -26,6 +26,7 @@ import type { WorkflowLogRow } from '@/lib/api/contracts/logs' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' import { cn } from '@/lib/core/utils/cn' import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans' +import { getDisplayStatus, StatusBadge, TriggerBadge } from '@/lib/logs/status' import type { TraceSpan } from '@/lib/logs/types' import { workflowBorderColor } from '@/lib/workspaces/colors' import { @@ -38,9 +39,6 @@ import { DELETED_WORKFLOW_COLOR, DELETED_WORKFLOW_LABEL, formatDate, - getDisplayStatus, - StatusBadge, - TriggerBadge, } from '@/app/workspace/[workspaceId]/logs/utils' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import { usePermissionConfig } from '@/hooks/use-permission-config' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx index 7bf398a99ca..e8261094955 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx @@ -9,15 +9,13 @@ import { Badge, buttonVariants, Loader } from '@/components/emcn' import type { WorkflowLogSummary } from '@/lib/api/contracts/logs' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' +import { getDisplayStatus, StatusBadge, TriggerBadge } from '@/lib/logs/status' import { workflowBorderColor } from '@/lib/workspaces/colors' import { DELETED_WORKFLOW_COLOR, DELETED_WORKFLOW_LABEL, formatDate, - getDisplayStatus, LOG_COLUMNS, - StatusBadge, - TriggerBadge, } from '@/app/workspace/[workspaceId]/logs/utils' const LOG_ROW_HEIGHT = 44 as const diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx index 33fce96c685..2e1eaef1df0 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx @@ -19,9 +19,9 @@ import { import { cn } from '@/lib/core/utils/cn' import { hasActiveFilters } from '@/lib/logs/filters' import { getTriggerOptions } from '@/lib/logs/get-trigger-options' +import { type LogStatus, STATUS_CONFIG } from '@/lib/logs/status' import { captureEvent } from '@/lib/posthog/client' import { workflowBorderColor } from '@/lib/workspaces/colors' -import { type LogStatus, STATUS_CONFIG } from '@/app/workspace/[workspaceId]/logs/utils' import { getBlock } from '@/blocks/registry' import { useFolderMap } from '@/hooks/queries/folders' import { useWorkflows } from '@/hooks/queries/workflows' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 5c3b3b0af66..8f610b46794 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'r import { formatDuration } from '@sim/utils/formatting' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'next/navigation' +import { parseAsString, useQueryState } from 'nuqs' import { useShallow } from 'zustand/react/shallow' import { Bell, @@ -280,10 +281,9 @@ export default function Logs() { selectedLogId: null, isSidebarOpen: false, }) - const [pendingExecutionId, setPendingExecutionId] = useState(() => - typeof window !== 'undefined' - ? new URLSearchParams(window.location.search).get('executionId') - : null + const [executionIdParam, setExecutionIdParam] = useQueryState( + 'executionId', + parseAsString.withOptions({ history: 'replace' }) ) const [searchQuery, setSearchQuery] = useState(() => { @@ -299,6 +299,7 @@ export default function Logs() { const logsRef = useRef([]) const selectedLogIndexRef = useRef(-1) const selectedLogIdRef = useRef(null) + const isSidebarOpenRef = useRef(false) const shouldScrollIntoViewRef = useRef(false) const logsRefetchRef = useRef<() => void>(() => {}) const activeLogRefetchRef = useRef<() => void>(() => {}) @@ -412,6 +413,7 @@ export default function Logs() { logsRef.current = logs selectedLogIndexRef.current = selectedLogIndex selectedLogIdRef.current = selectedLogId + isSidebarOpenRef.current = isSidebarOpen logsRefetchRef.current = logsQuery.refetch activeLogRefetchRef.current = selectedDetailQuery.refetch logsQueryRef.current = { @@ -420,18 +422,26 @@ export default function Logs() { fetchNextPage: logsQuery.fetchNextPage, } - const deepLinkQuery = useLogByExecutionId(workspaceId, pendingExecutionId) + const deepLinkQuery = useLogByExecutionId(workspaceId, executionIdParam) + const prevExecutionIdParamRef = useRef(executionIdParam) useEffect(() => { - if (!pendingExecutionId) return + const prev = prevExecutionIdParamRef.current + prevExecutionIdParamRef.current = executionIdParam + + if (!executionIdParam) { + if (prev && isSidebarOpenRef.current) { + dispatch({ type: 'CLOSE_SIDEBAR' }) + } + return + } const resolvedId = deepLinkQuery.data?.id - if (resolvedId) { + if (resolvedId && (resolvedId !== selectedLogIdRef.current || !isSidebarOpenRef.current)) { dispatch({ type: 'TOGGLE_LOG', logId: resolvedId }) - setPendingExecutionId(null) } else if (deepLinkQuery.isError) { - setPendingExecutionId(null) + setExecutionIdParam(null) } - }, [pendingExecutionId, deepLinkQuery.data, deepLinkQuery.isError]) + }, [executionIdParam, deepLinkQuery.data, deepLinkQuery.isError, setExecutionIdParam]) useEffect(() => { const timers = refreshTimersRef.current @@ -445,31 +455,50 @@ export default function Logs() { setStoreSearchQuery(debouncedSearchQuery) }, [debouncedSearchQuery, setStoreSearchQuery]) - const handleLogClick = useCallback((rowId: string) => { - dispatch({ type: 'TOGGLE_LOG', logId: rowId }) - }, []) + const handleLogClick = useCallback( + (rowId: string) => { + const isClosing = selectedLogIdRef.current === rowId && isSidebarOpenRef.current + dispatch({ type: 'TOGGLE_LOG', logId: rowId }) + if (isClosing) { + setExecutionIdParam(null) + } else { + const log = logsRef.current.find((l) => l.id === rowId) + setExecutionIdParam(log?.executionId ?? null) + } + }, + [setExecutionIdParam] + ) const handleNavigateNext = useCallback(() => { const idx = selectedLogIndexRef.current const currentLogs = logsRef.current if (idx >= 0 && idx < currentLogs.length - 1) { + const next = currentLogs[idx + 1] shouldScrollIntoViewRef.current = true - dispatch({ type: 'SELECT_LOG', logId: currentLogs[idx + 1].id }) + dispatch({ type: 'SELECT_LOG', logId: next.id }) + if (isSidebarOpenRef.current) { + setExecutionIdParam(next.executionId ?? null) + } } - }, []) + }, [setExecutionIdParam]) const handleNavigatePrev = useCallback(() => { const idx = selectedLogIndexRef.current if (idx > 0) { + const prev = logsRef.current[idx - 1] shouldScrollIntoViewRef.current = true - dispatch({ type: 'SELECT_LOG', logId: logsRef.current[idx - 1].id }) + dispatch({ type: 'SELECT_LOG', logId: prev.id }) + if (isSidebarOpenRef.current) { + setExecutionIdParam(prev.executionId ?? null) + } } - }, []) + }, [setExecutionIdParam]) const handleCloseSidebar = useCallback(() => { dispatch({ type: 'CLOSE_SIDEBAR' }) + setExecutionIdParam(null) activeLogTabRef.current = 'overview' - }, []) + }, [setExecutionIdParam]) const handleActiveTabChange = useCallback((tab: string) => { activeLogTabRef.current = tab @@ -722,13 +751,20 @@ export default function Logs() { if (e.key === 'Enter' && selectedLogIdRef.current) { e.preventDefault() + const willOpen = !isSidebarOpenRef.current dispatch({ type: 'TOGGLE_SIDEBAR' }) + if (willOpen) { + const log = logsRef.current.find((l) => l.id === selectedLogIdRef.current) + setExecutionIdParam(log?.executionId ?? null) + } else { + setExecutionIdParam(null) + } } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [handleNavigateNext, handleNavigatePrev]) + }, [handleNavigateNext, handleNavigatePrev, setExecutionIdParam]) const handleCloseContextMenu = useCallback(() => setContextMenuOpen(false), []) const handleOpenNotificationSettings = useCallback(() => setIsNotificationSettingsOpen(true), []) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index 8fa8f4624a6..b216586dabd 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -1,11 +1,6 @@ -import React from 'react' import { formatDuration } from '@sim/utils/formatting' import { format } from 'date-fns' -import { Badge } from '@/components/emcn' import type { WorkflowLogDetail } from '@/lib/api/contracts/logs' -import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options' -import { getBlock } from '@/blocks/registry' -import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types' export const LOG_COLUMNS = { workflow: { width: 'w-[22%]', minWidth: 'min-w-[140px]', label: 'Workflow' }, @@ -19,129 +14,6 @@ export const LOG_COLUMNS = { export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow' export const DELETED_WORKFLOW_COLOR = 'var(--text-tertiary)' -export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled' | 'cancelling' - -/** - * Maps raw status string to LogStatus for display. - * @param status - Raw status from API - * @returns Normalized LogStatus value - */ -export function getDisplayStatus(status: string | null | undefined): LogStatus { - switch (status) { - case 'running': - return 'running' - case 'pending': - return 'pending' - case 'cancelling': - return 'cancelling' - case 'cancelled': - return 'cancelled' - case 'failed': - return 'error' - default: - return 'info' - } -} - -export const STATUS_CONFIG: Record< - LogStatus, - { - variant: React.ComponentProps['variant'] - label: string - color: string - /** Whether this status appears as a filter option. Intermediary states (e.g. cancelling) are excluded. */ - filterable: boolean - } -> = { - error: { variant: 'red', label: 'Error', color: 'var(--text-error)', filterable: true }, - pending: { variant: 'amber', label: 'Pending', color: '#f59e0b', filterable: true }, - running: { variant: 'amber', label: 'Running', color: '#f59e0b', filterable: true }, - cancelling: { variant: 'amber', label: 'Cancelling...', color: '#f59e0b', filterable: false }, - cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316', filterable: true }, - info: { - variant: 'gray', - label: 'Info', - color: 'var(--terminal-status-info-color)', - filterable: true, - }, -} - -const TRIGGER_VARIANT_MAP: Record['variant']> = { - manual: 'gray-secondary', - api: 'blue', - schedule: 'green', - chat: 'purple', - webhook: 'orange', - mcp: 'cyan', - a2a: 'teal', - copilot: 'pink', - mothership: 'pink', - workflow: 'blue-secondary', -} - -interface StatusBadgeProps { - status: LogStatus -} - -/** - * Renders a colored badge indicating log execution status. - * @param props - Component props containing the status - * @returns A Badge with dot indicator and status label - */ -export function StatusBadge({ status }: StatusBadgeProps) { - const config = STATUS_CONFIG[status] - return React.createElement( - Badge, - { variant: config.variant, dot: true, size: 'sm' }, - config.label - ) -} - -interface TriggerBadgeProps { - trigger: string -} - -/** - * Renders a colored badge indicating the workflow trigger type. - * Core triggers display with their designated colors; integrations show with icons. - * @param props - Component props containing the trigger type - * @returns A Badge with appropriate styling for the trigger type - */ -export function TriggerBadge({ trigger }: TriggerBadgeProps) { - const metadata = getIntegrationMetadata(trigger) - const isIntegration = !(CORE_TRIGGER_TYPES as readonly string[]).includes(trigger) - const block = isIntegration ? getBlock(trigger) : null - const IconComponent = block?.icon - - const coreVariant = TRIGGER_VARIANT_MAP[trigger] - if (coreVariant) { - return React.createElement( - Badge, - { variant: coreVariant, size: 'sm', className: 'whitespace-nowrap' }, - metadata.label - ) - } - - if (IconComponent) { - return React.createElement( - Badge, - { - variant: 'gray-secondary', - size: 'sm', - icon: IconComponent, - className: 'whitespace-nowrap', - }, - metadata.label - ) - } - - return React.createElement( - Badge, - { variant: 'gray-secondary', size: 'sm', className: 'whitespace-nowrap' }, - metadata.label - ) -} - interface LogWithDuration { totalDurationMs?: number | string duration?: number | string diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/index.ts index a9946f7d520..6ccda57246c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/index.ts @@ -1 +1,2 @@ +export { Logs } from './logs' export { Versions } from './versions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/logs.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/logs.tsx new file mode 100644 index 00000000000..aaff98564c0 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/logs.tsx @@ -0,0 +1,179 @@ +'use client' + +import { useMemo } from 'react' +import { formatDateTime, formatDuration } from '@sim/utils/formatting' +import clsx from 'clsx' +import { useParams, useRouter } from 'next/navigation' +import { Skeleton } from '@/components/emcn' +import type { WorkflowLogSummary } from '@/lib/api/contracts/logs' +import { getDisplayStatus, StatusBadge, TriggerBadge } from '@/lib/logs/status' +import { type LogFilters, useLogsList } from '@/hooks/queries/logs' + +const HEADER_TEXT_CLASS = 'font-medium text-[var(--text-tertiary)] text-caption' +const ROW_TEXT_CLASS = 'font-medium text-[var(--text-primary)] text-caption' +const COLUMN_BASE_CLASS = 'flex-shrink-0' + +const COLUMN_WIDTHS = { + STATUS: 'w-[100px]', + TRIGGER: 'w-[120px]', + DURATION: 'w-[80px]', + TIMESTAMP: 'flex-1', +} as const + +const LOGS_LIMIT = 5 as const + +const BASE_FILTERS = { + timeRange: 'All time', + level: 'all', + folderIds: [] as string[], + triggers: [] as string[], + searchQuery: '', + limit: LOGS_LIMIT, + sortBy: 'date', + sortOrder: 'desc', +} as const satisfies Omit + +interface LogsProps { + workflowId: string | null +} + +/** + * Displays the latest workflow runs inside the deploy modal. + * Clicking a row opens that execution in the Logs page. + */ +export function Logs({ workflowId }: LogsProps) { + const params = useParams() + const router = useRouter() + const workspaceId = params?.workspaceId as string | undefined + + const filters = useMemo( + () => ({ ...BASE_FILTERS, workflowIds: workflowId ? [workflowId] : [] }), + [workflowId] + ) + + const { data, isLoading } = useLogsList(workspaceId, filters, { + enabled: Boolean(workflowId) && Boolean(workspaceId), + }) + + const logs = useMemo( + () => (data?.pages?.[0]?.logs ?? []).slice(0, LOGS_LIMIT), + [data] + ) + + const handleRowClick = (log: WorkflowLogSummary) => { + if (!workspaceId || !log.executionId) return + router.push(`/workspace/${workspaceId}/logs?executionId=${log.executionId}`) + } + + if (isLoading && logs.length === 0) { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ {Array.from({ length: LOGS_LIMIT }, (_, i) => i).map((i) => ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ))} +
+
+ ) + } + + if (logs.length === 0) { + return ( +
+ No runs yet +
+ ) + } + + return ( +
+
+
+ Status +
+
+ Trigger +
+
+ Duration +
+
+ Timestamp +
+
+ +
+ {logs.map((log) => { + const isClickable = Boolean(log.executionId && workspaceId) + return ( +
handleRowClick(log) : undefined} + > +
+ +
+ +
+ {log.trigger ? ( + + ) : ( + + )} +
+ +
+ + {formatDuration(log.duration, { precision: 2 }) || '—'} + +
+ +
+ + {formatDateTime(new Date(log.createdAt))} + +
+
+ ) + })} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx index 695d6c95cf5..5d974c4439d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/general.tsx @@ -20,7 +20,7 @@ import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persiste import { Preview, PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview' import { useDeploymentVersionState, useRevertToVersion } from '@/hooks/queries/workflows' import type { WorkflowState } from '@/stores/workflows/workflow/types' -import { Versions } from './components' +import { Logs, Versions } from './components' const logger = createLogger('GeneralDeploy') @@ -227,6 +227,13 @@ export function GeneralDeploy({ onLoadDeployment={handleLoadDeployment} /> + +
+ + +
diff --git a/apps/sim/lib/logs/status.tsx b/apps/sim/lib/logs/status.tsx new file mode 100644 index 00000000000..146dff9adc4 --- /dev/null +++ b/apps/sim/lib/logs/status.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import { Badge } from '@/components/emcn' +import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options' +import { getBlock } from '@/blocks/registry' +import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types' + +export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled' | 'cancelling' + +/** + * Maps raw status string to LogStatus for display. + * @param status - Raw status from API + * @returns Normalized LogStatus value + */ +export function getDisplayStatus(status: string | null | undefined): LogStatus { + switch (status) { + case 'running': + return 'running' + case 'pending': + return 'pending' + case 'cancelling': + return 'cancelling' + case 'cancelled': + return 'cancelled' + case 'failed': + return 'error' + default: + return 'info' + } +} + +export const STATUS_CONFIG: Record< + LogStatus, + { + variant: React.ComponentProps['variant'] + label: string + color: string + /** Whether this status appears as a filter option. Intermediary states (e.g. cancelling) are excluded. */ + filterable: boolean + } +> = { + error: { variant: 'red', label: 'Error', color: 'var(--text-error)', filterable: true }, + pending: { variant: 'amber', label: 'Pending', color: '#f59e0b', filterable: true }, + running: { variant: 'amber', label: 'Running', color: '#f59e0b', filterable: true }, + cancelling: { variant: 'amber', label: 'Cancelling...', color: '#f59e0b', filterable: false }, + cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316', filterable: true }, + info: { + variant: 'gray', + label: 'Info', + color: 'var(--terminal-status-info-color)', + filterable: true, + }, +} + +const TRIGGER_VARIANT_MAP: Record['variant']> = { + manual: 'gray-secondary', + api: 'blue', + schedule: 'green', + chat: 'purple', + webhook: 'orange', + mcp: 'cyan', + a2a: 'teal', + copilot: 'pink', + mothership: 'pink', + workflow: 'blue-secondary', +} + +interface StatusBadgeProps { + status: LogStatus +} + +/** + * Renders a colored badge indicating log execution status. + * @param props - Component props containing the status + * @returns A Badge with dot indicator and status label + */ +export function StatusBadge({ status }: StatusBadgeProps) { + const config = STATUS_CONFIG[status] + return React.createElement( + Badge, + { variant: config.variant, dot: true, size: 'sm' }, + config.label + ) +} + +interface TriggerBadgeProps { + trigger: string +} + +/** + * Renders a colored badge indicating the workflow trigger type. + * Core triggers display with their designated colors; integrations show with icons. + * @param props - Component props containing the trigger type + * @returns A Badge with appropriate styling for the trigger type + */ +export function TriggerBadge({ trigger }: TriggerBadgeProps) { + const metadata = getIntegrationMetadata(trigger) + const isIntegration = !(CORE_TRIGGER_TYPES as readonly string[]).includes(trigger) + const block = isIntegration ? getBlock(trigger) : null + const IconComponent = block?.icon + + const coreVariant = TRIGGER_VARIANT_MAP[trigger] + if (coreVariant) { + return React.createElement( + Badge, + { variant: coreVariant, size: 'sm', className: 'whitespace-nowrap' }, + metadata.label + ) + } + + if (IconComponent) { + return React.createElement( + Badge, + { + variant: 'gray-secondary', + size: 'sm', + icon: IconComponent, + className: 'whitespace-nowrap', + }, + metadata.label + ) + } + + return React.createElement( + Badge, + { variant: 'gray-secondary', size: 'sm', className: 'whitespace-nowrap' }, + metadata.label + ) +} diff --git a/apps/sim/package.json b/apps/sim/package.json index 2304b992c53..55487269b3e 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -65,6 +65,7 @@ "@modelcontextprotocol/sdk": "1.29.0", "@monaco-editor/react": "4.7.0", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "2.1.0", "@opentelemetry/exporter-jaeger": "2.1.0", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", "@opentelemetry/resources": "^2.0.0", @@ -77,6 +78,7 @@ "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.5", @@ -165,6 +167,7 @@ "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", "nodemailer": "8.0.7", + "nuqs": "2.8.9", "officeparser": "^5.2.0", "openai": "^4.91.1", "papaparse": "5.5.3", diff --git a/apps/sim/stores/logs/filters/store.ts b/apps/sim/stores/logs/filters/store.ts index 4d9ffee18cf..88a91fc6954 100644 --- a/apps/sim/stores/logs/filters/store.ts +++ b/apps/sim/stores/logs/filters/store.ts @@ -19,6 +19,17 @@ const updateURL = (params: URLSearchParams) => { window.history.replaceState({}, '', url) } +const FILTER_PARAM_KEYS = [ + 'timeRange', + 'startDate', + 'endDate', + 'level', + 'workflowIds', + 'folderIds', + 'triggers', + 'search', +] as const + const DEFAULT_TIME_RANGE: TimeRange = 'All time' const parseTimeRangeFromURL = (value: string | null): TimeRange => { @@ -274,7 +285,10 @@ export const useFilterStore = create((set, get) => ({ syncWithURL: () => { const { timeRange, startDate, endDate, level, workflowIds, folderIds, triggers, searchQuery } = get() - const params = new URLSearchParams() + const params = getSearchParams() + for (const key of FILTER_PARAM_KEYS) { + params.delete(key) + } if (timeRange !== DEFAULT_TIME_RANGE) { params.set('timeRange', timeRangeToURL(timeRange)) diff --git a/bun.lock b/bun.lock index 3c658d61d32..e5d57f1d71e 100644 --- a/bun.lock +++ b/bun.lock @@ -119,6 +119,7 @@ "@modelcontextprotocol/sdk": "1.29.0", "@monaco-editor/react": "4.7.0", "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "2.1.0", "@opentelemetry/exporter-jaeger": "2.1.0", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", "@opentelemetry/resources": "^2.0.0", @@ -131,6 +132,7 @@ "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.5", @@ -219,6 +221,7 @@ "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", "nodemailer": "8.0.7", + "nuqs": "2.8.9", "officeparser": "^5.2.0", "openai": "^4.91.1", "papaparse": "5.5.3", @@ -3240,6 +3243,8 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "nuqs": ["nuqs@2.8.9", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "peerDependencies": { "@remix-run/react": ">=2", "@tanstack/react-router": "^1", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router": "^5 || ^6 || ^7", "react-router-dom": "^5 || ^6 || ^7" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ=="], + "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], "nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="], @@ -4792,6 +4797,8 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "nuqs/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "nypm/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "oauth2-mock-server/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],