diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/control-bar/control-bar.tsx deleted file mode 100644 index 3aa4dddc9..000000000 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/control-bar/control-bar.tsx +++ /dev/null @@ -1,170 +0,0 @@ -'use client' - -import { useEffect, useRef, useState } from 'react' -import { Loader2, Play, RefreshCw, Search, Square } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { createLogger } from '@/lib/logs/console-logger' -import { useDebounce } from '@/hooks/use-debounce' -import { useFilterStore } from '../../stores/store' -import type { LogsResponse } from '../../stores/types' - -const logger = createLogger('ControlBar') - -/** - * Control bar for logs page - includes search functionality and refresh/live controls - */ -export function ControlBar() { - const [isLive, setIsLive] = useState(false) - const [searchQuery, setSearchQuery] = useState('') - const [isRefreshing, setIsRefreshing] = useState(false) - const liveIntervalRef = useRef(null) - const debouncedSearchQuery = useDebounce(searchQuery, 300) - const { - setSearchQuery: setStoreSearchQuery, - setLogs, - setError, - buildQueryParams, - } = useFilterStore() - - // Update store when debounced search query changes - useEffect(() => { - setStoreSearchQuery(debouncedSearchQuery) - }, [debouncedSearchQuery, setStoreSearchQuery]) - - const fetchLogs = async () => { - try { - const queryParams = buildQueryParams(1, 50) - const response = await fetch(`/api/logs/enhanced?${queryParams}`) - - if (!response.ok) { - throw new Error(`Error fetching logs: ${response.statusText}`) - } - - const data: LogsResponse = await response.json() - return data - } catch (err) { - logger.error('Failed to fetch logs:', { err }) - throw err - } - } - - const handleRefresh = async () => { - if (isRefreshing) return - - setIsRefreshing(true) - - // Create a timer to ensure the spinner shows for at least 1 second - const minLoadingTime = new Promise((resolve) => setTimeout(resolve, 1000)) - - try { - // Fetch new logs - const logsResponse = await fetchLogs() - - // Wait for minimum loading time - await minLoadingTime - - // Replace logs with fresh filtered results from server - setLogs(logsResponse.data) - setError(null) - } catch (err) { - // Wait for minimum loading time - await minLoadingTime - - setError(err instanceof Error ? err.message : 'An unknown error occurred') - } finally { - setIsRefreshing(false) - } - } - - // Setup or clear the live refresh interval when isLive changes - useEffect(() => { - // Clear any existing interval - if (liveIntervalRef.current) { - clearInterval(liveIntervalRef.current) - liveIntervalRef.current = null - } - - // If live mode is active, set up the interval - if (isLive) { - // Initial refresh when live mode is activated - handleRefresh() - - // Set up interval for subsequent refreshes (every 5 seconds) - liveIntervalRef.current = setInterval(() => { - handleRefresh() - }, 5000) - } - - // Cleanup function to clear interval when component unmounts or isLive changes - return () => { - if (liveIntervalRef.current) { - clearInterval(liveIntervalRef.current) - liveIntervalRef.current = null - } - } - }, [isLive]) - - const toggleLive = () => { - setIsLive(!isLive) - } - - return ( -
- {/* Left Section - Search */} -
-
- -
- setSearchQuery(e.target.value)} - /> -
- - {/* Middle Section - Reserved for future use */} -
- - {/* Right Section - Actions */} -
- - - - - {isRefreshing ? 'Refreshing...' : 'Refresh'} - - - -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx index ab3b722d2..c5fad1ef7 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/filter-section.tsx @@ -1,41 +1,20 @@ -import { useState } from 'react' -import { ChevronDown } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' - export default function FilterSection({ title, - defaultOpen = false, content, }: { title: string - defaultOpen?: boolean content?: React.ReactNode }) { - const [isOpen, setIsOpen] = useState(defaultOpen) - return ( - - - - - +
+
{title}
+
{content || (
Filter options for {title} will go here
)} - - +
+
) } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx index bc7432758..28fcc6851 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/folder.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Check, ChevronDown, Folder } from 'lucide-react' +import { Check, ChevronDown } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { @@ -9,8 +9,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store' import { useFolderStore } from '@/stores/folders/store' +import { useFilterStore } from '@/stores/logs/filters/store' interface FolderOption { id: string @@ -91,49 +91,35 @@ export default function FolderFilter() { setFolderIds([]) } - // Add special option for workflows without folders - const includeRootOption = true - return ( - - + { e.preventDefault() clearSelections() }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' > All folders {folderIds.length === 0 && } - {/* Option for workflows without folders */} - {includeRootOption && ( - { - e.preventDefault() - toggleFolderId('root') - }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' - > -
- - No folder -
- {isFolderSelected('root') && } -
- )} - - {(!loading && folders.length > 0) || includeRootOption ? : null} + {!loading && folders.length > 0 && } {!loading && folders.map((folder) => ( @@ -143,13 +129,9 @@ export default function FolderFilter() { e.preventDefault() toggleFolderId(folder.id) }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' >
-
{folder.path} @@ -159,7 +141,10 @@ export default function FolderFilter() { ))} {loading && ( - + Loading folders... )} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx index c83706a5b..78b119229 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/level.tsx @@ -4,46 +4,66 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store' -import type { LogLevel } from '@/app/workspace/[workspaceId]/logs/stores/types' +import { useFilterStore } from '@/stores/logs/filters/store' +import type { LogLevel } from '@/stores/logs/filters/types' export default function Level() { const { level, setLevel } = useFilterStore() - const levels: { value: LogLevel; label: string; color?: string }[] = [ - { value: 'all', label: 'Any status' }, + const specificLevels: { value: LogLevel; label: string; color: string }[] = [ { value: 'error', label: 'Error', color: 'bg-destructive/100' }, { value: 'info', label: 'Info', color: 'bg-muted-foreground/100' }, ] const getDisplayLabel = () => { - const selected = levels.find((l) => l.value === level) + if (level === 'all') return 'Any status' + const selected = specificLevels.find((l) => l.value === level) return selected ? selected.label : 'Any status' } return ( - - - {levels.map((levelItem) => ( + + { + e.preventDefault() + setLevel('all') + }} + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' + > + Any status + {level === 'all' && } + + + + + {specificLevels.map((levelItem) => ( { e.preventDefault() setLevel(levelItem.value) }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' >
- {levelItem.color && ( -
- )} +
{levelItem.label}
{level === levelItem.value && } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx index 0a475b6f1..b01788ede 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/timeline.tsx @@ -4,32 +4,54 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store' -import type { TimeRange } from '@/app/workspace/[workspaceId]/logs/stores/types' +import { useFilterStore } from '@/stores/logs/filters/store' +import type { TimeRange } from '@/stores/logs/filters/types' export default function Timeline() { const { timeRange, setTimeRange } = useFilterStore() - const timeRanges: TimeRange[] = ['All time', 'Past 30 minutes', 'Past hour', 'Past 24 hours'] + const specificTimeRanges: TimeRange[] = ['Past 30 minutes', 'Past hour', 'Past 24 hours'] return ( - - - {timeRanges.map((range) => ( + + { + e.preventDefault() + setTimeRange('All time') + }} + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' + > + All time + {timeRange === 'All time' && } + + + + + {specificTimeRanges.map((range) => ( { e.preventDefault() setTimeRange(range) }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' > {range} {timeRange === range && } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx index 1cabe0583..e3e053daa 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx @@ -7,13 +7,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store' -import type { TriggerType } from '../../../stores/types' +import { useFilterStore } from '@/stores/logs/filters/store' +import type { TriggerType } from '../../../../../../../stores/logs/filters/types' export default function Trigger() { const { triggers, toggleTrigger, setTriggers } = useFilterStore() const triggerOptions: { value: TriggerType; label: string; color?: string }[] = [ - { value: 'manual', label: 'Manual', color: 'bg-secondary' }, + { value: 'manual', label: 'Manual', color: 'bg-gray-500' }, { value: 'api', label: 'API', color: 'bg-blue-500' }, { value: 'webhook', label: 'Webhook', color: 'bg-orange-500' }, { value: 'schedule', label: 'Schedule', color: 'bg-green-500' }, @@ -43,19 +43,26 @@ export default function Trigger() { return ( - - + { e.preventDefault() clearSelections() }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' > All triggers {triggers.length === 0 && } @@ -70,7 +77,7 @@ export default function Trigger() { e.preventDefault() toggleTrigger(triggerItem.value) }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' >
{triggerItem.color && ( diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx index 47081d316..4570693a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/workflow.tsx @@ -8,7 +8,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { useFilterStore } from '@/app/workspace/[workspaceId]/logs/stores/store' +import { useFilterStore } from '@/stores/logs/filters/store' interface WorkflowOption { id: string @@ -69,19 +69,30 @@ export default function Workflow() { return ( - - + { e.preventDefault() clearSelections() }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' > All workflows {workflowIds.length === 0 && } @@ -97,7 +108,7 @@ export default function Workflow() { e.preventDefault() toggleWorkflowId(workflow.id) }} - className='flex cursor-pointer items-center justify-between p-2 text-sm' + className='flex cursor-pointer items-center justify-between rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50' >
+ Loading workflows... )} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx index 95c228d3d..1b783a051 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/filters/filters.tsx @@ -57,19 +57,19 @@ export function Filters() {

Filters

{/* Timeline Filter */} - } /> + } /> {/* Level Filter */} - } /> + } /> {/* Trigger Filter */} - } /> + } /> {/* Folder Filter */} - } /> + } /> {/* Workflow Filter */} - } /> + } />
) } diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx index 69184b729..0768cac38 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx @@ -7,9 +7,9 @@ import { CopyButton } from '@/components/ui/copy-button' import { ScrollArea } from '@/components/ui/scroll-area' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { redactApiKeys } from '@/lib/utils' -import type { WorkflowLog } from '@/app/workspace/[workspaceId]/logs/stores/types' import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date' import { formatCost } from '@/providers/utils' +import type { WorkflowLog } from '@/stores/logs/filters/types' import { FrozenCanvasModal } from '../frozen-canvas/frozen-canvas-modal' import { ToolCallsDisplay } from '../tool-calls/tool-calls-display' import { TraceSpansDisplay } from '../trace-spans/trace-spans-display' @@ -350,10 +350,10 @@ export function Sidebar({ return (
{/* Header */} -
-

Log Details

-
+
+

Log Details

+
@@ -414,277 +414,280 @@ export function Sidebar({
{/* Content */} - -
- {/* Timestamp */} -
-

Timestamp

-
- - {formatDate(log.createdAt).full} -
-
- - {/* Workflow */} - {log.workflow && ( +
+ +
+ {/* Timestamp */}
-

Workflow

-
- +

Timestamp

+
+ + {formatDate(log.createdAt).full} +
+
+ + {/* Workflow */} + {log.workflow && ( +
+

Workflow

- {log.workflow.name} + +
+ {log.workflow.name} +
-
- )} + )} - {/* Execution ID */} - {log.executionId && ( -
-

Execution ID

-
- - {log.executionId} + {/* Execution ID */} + {log.executionId && ( +
+

Execution ID

+
+ + {log.executionId} +
-
- )} + )} - {/* Level */} -
-

Level

-
- - {log.level} -
-
- - {/* Trigger */} - {log.trigger && ( + {/* Level */}
-

Trigger

+

Level

- - {log.trigger} + + {log.level}
- )} - {/* Duration */} - {log.duration && ( -
-

Duration

-
- - {log.duration} -
-
- )} - - {/* Enhanced Cost - only show for enhanced logs with actual cost data */} - {log.metadata?.enhanced && hasCostInfo && ( -
-

Cost Breakdown

-
- {(log.metadata?.cost?.total ?? 0) > 0 && ( -
- Total Cost: - - ${log.metadata?.cost?.total?.toFixed(4)} - -
- )} - {(log.metadata?.cost?.input ?? 0) > 0 && ( -
- Input Cost: - - ${log.metadata?.cost?.input?.toFixed(4)} - -
- )} - {(log.metadata?.cost?.output ?? 0) > 0 && ( -
- Output Cost: - - ${log.metadata?.cost?.output?.toFixed(4)} - -
- )} - {(log.metadata?.cost?.tokens?.total ?? 0) > 0 && ( -
- Total Tokens: - - {log.metadata?.cost?.tokens?.total?.toLocaleString()} - -
- )} -
-
- )} - - {/* Frozen Canvas Button - only show for workflow execution logs with execution ID */} - {isWorkflowExecutionLog && log.executionId && ( -
-

Workflow State

- -

- See the exact workflow state and block inputs/outputs at execution time -

-
- )} - - {/* Message Content */} -
-

Message

-
{formattedContent}
-
- - {/* Trace Spans (if available and this is a workflow execution log) */} - {isWorkflowExecutionLog && log.metadata?.traceSpans && ( -
-
- -
-
- )} - - {/* Tool Calls (if available) */} - {log.metadata?.toolCalls && log.metadata.toolCalls.length > 0 && ( -
-

Tool Calls

-
- -
-
- )} - - {/* Cost Information (moved to bottom) */} - {hasCostInfo && ( -
-

Models

-
-
-
- Input: - - {formatCost(log.metadata?.cost?.input || 0)} - -
-
- Output: - - {formatCost(log.metadata?.cost?.output || 0)} - -
-
- Total: - - {formatCost(log.metadata?.cost?.total || 0)} - -
-
- Tokens: - - {log.metadata?.cost?.tokens?.prompt || 0} in /{' '} - {log.metadata?.cost?.tokens?.completion || 0} out - -
+ {/* Trigger */} + {log.trigger && ( +
+

Trigger

+
+ + {log.trigger}
+
+ )} - {/* Models Breakdown */} - {log.metadata?.cost?.models && - Object.keys(log.metadata?.cost?.models).length > 0 && ( -
- + {/* Duration */} + {log.duration && ( +
+

Duration

+
+ + {log.duration} +
+
+ )} - {isModelsExpanded && ( -
- {Object.entries(log.metadata?.cost?.models || {}).map( - ([model, cost]: [string, any]) => ( -
-
{model}
-
-
- Input: - {formatCost(cost.input || 0)} -
-
- Output: - {formatCost(cost.output || 0)} -
-
- Total: - - {formatCost(cost.total || 0)} - -
-
- Tokens: - - {cost.tokens?.prompt || 0} in /{' '} - {cost.tokens?.completion || 0} out - -
-
-
- ) - )} -
- )} + {/* Enhanced Cost - only show for enhanced logs with actual cost data */} + {log.metadata?.enhanced && hasCostInfo && ( +
+

+ Cost Breakdown +

+
+ {(log.metadata?.cost?.total ?? 0) > 0 && ( +
+ Total Cost: + + ${log.metadata?.cost?.total?.toFixed(4)} +
)} - - {isWorkflowWithCost && ( -
-

- This is the total cost for all LLM-based blocks in this workflow - execution. -

-
- )} + {(log.metadata?.cost?.input ?? 0) > 0 && ( +
+ Input Cost: + + ${log.metadata?.cost?.input?.toFixed(4)} + +
+ )} + {(log.metadata?.cost?.output ?? 0) > 0 && ( +
+ Output Cost: + + ${log.metadata?.cost?.output?.toFixed(4)} + +
+ )} + {(log.metadata?.cost?.tokens?.total ?? 0) > 0 && ( +
+ Total Tokens: + + {log.metadata?.cost?.tokens?.total?.toLocaleString()} + +
+ )} +
+ )} + + {/* Frozen Canvas Button - only show for workflow execution logs with execution ID */} + {isWorkflowExecutionLog && log.executionId && ( +
+

+ Workflow State +

+ +

+ See the exact workflow state and block inputs/outputs at execution time +

+
+ )} + + {/* Message Content */} +
+

Message

+
{formattedContent}
- )} -
- + + {/* Trace Spans (if available and this is a workflow execution log) */} + {isWorkflowExecutionLog && log.metadata?.traceSpans && ( +
+
+ +
+
+ )} + + {/* Tool Calls (if available) */} + {log.metadata?.toolCalls && log.metadata.toolCalls.length > 0 && ( +
+

Tool Calls

+
+ +
+
+ )} + + {/* Cost Information (moved to bottom) */} + {hasCostInfo && ( +
+

Models

+
+
+
+ Input: + + {formatCost(log.metadata?.cost?.input || 0)} + +
+
+ Output: + + {formatCost(log.metadata?.cost?.output || 0)} + +
+
+ Total: + + {formatCost(log.metadata?.cost?.total || 0)} + +
+
+ Tokens: + + {log.metadata?.cost?.tokens?.prompt || 0} in /{' '} + {log.metadata?.cost?.tokens?.completion || 0} out + +
+
+ + {/* Models Breakdown */} + {log.metadata?.cost?.models && + Object.keys(log.metadata?.cost?.models).length > 0 && ( +
+ + + {isModelsExpanded && ( +
+ {Object.entries(log.metadata?.cost?.models || {}).map( + ([model, cost]: [string, any]) => ( +
+
{model}
+
+
+ Input: + {formatCost(cost.input || 0)} +
+
+ Output: + {formatCost(cost.output || 0)} +
+
+ Total: + + {formatCost(cost.total || 0)} + +
+
+ Tokens: + + {cost.tokens?.prompt || 0} in /{' '} + {cost.tokens?.completion || 0} out + +
+
+
+ ) + )} +
+ )} +
+ )} + + {isWorkflowWithCost && ( +
+

+ This is the total cost for all LLM-based blocks in this workflow + execution. +

+
+ )} +
+
+ )} +
+ +
)} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx index fdf6ca08c..39191fb37 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { AlertCircle, CheckCircle2, ChevronDown, ChevronRight, Clock } from 'lucide-react' import { CopyButton } from '@/components/ui/copy-button' import { cn } from '@/lib/utils' -import type { ToolCall, ToolCallMetadata } from '../../stores/types' +import type { ToolCall, ToolCallMetadata } from '../../../../../../stores/logs/filters/types' interface ToolCallsDisplayProps { metadata: ToolCallMetadata diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx index 795a52ecc..79344b8a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display.tsx @@ -11,7 +11,7 @@ import { ConnectIcon, } from '@/components/icons' import { cn, redactApiKeys } from '@/lib/utils' -import type { TraceSpan } from '../../stores/types' +import type { TraceSpan } from '../../../../../../stores/logs/filters/types' interface TraceSpansDisplayProps { traceSpans?: TraceSpan[] diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 4df77a755..4651e734b 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -1,19 +1,42 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' -import { AlertCircle, Info, Loader2 } from 'lucide-react' +import { AlertCircle, Info, Loader2, Play, RefreshCw, Search, Square } from 'lucide-react' import { useParams } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { createLogger } from '@/lib/logs/console-logger' -import { ControlBar } from './components/control-bar/control-bar' -import { Filters } from './components/filters/filters' +import { cn } from '@/lib/utils' +import { useDebounce } from '@/hooks/use-debounce' +import { useFilterStore } from '../../../../stores/logs/filters/store' +import type { LogsResponse, WorkflowLog } from '../../../../stores/logs/filters/types' import { Sidebar } from './components/sidebar/sidebar' -import { useFilterStore } from './stores/store' -import type { LogsResponse, WorkflowLog } from './stores/types' import { formatDate } from './utils/format-date' const logger = createLogger('Logs') const LOGS_PER_PAGE = 50 +// Get color for different trigger types using app's color scheme +const getTriggerColor = (trigger: string | null | undefined): string => { + if (!trigger) return '#9ca3af' + + switch (trigger.toLowerCase()) { + case 'manual': + return '#9ca3af' // gray-400 (matches secondary styling better) + case 'schedule': + return '#10b981' // green (emerald-500) + case 'webhook': + return '#f97316' // orange (orange-500) + case 'chat': + return '#8b5cf6' // purple (violet-500) + case 'api': + return '#3b82f6' // blue (blue-500) + default: + return '#9ca3af' // gray-400 + } +} + const selectedRowAnimation = ` @keyframes borderPulse { 0% { border-left-color: hsl(var(--primary) / 0.3) } @@ -45,11 +68,13 @@ export default function Logs() { isFetchingMore, setIsFetchingMore, buildQueryParams, + initializeFromURL, timeRange, level, workflowIds, folderIds, - searchQuery, + searchQuery: storeSearchQuery, + setSearchQuery: setStoreSearchQuery, triggers, } = useFilterStore() @@ -64,6 +89,28 @@ export default function Logs() { const selectedRowRef = useRef(null) const loaderRef = useRef(null) const scrollContainerRef = useRef(null) + const isInitialized = useRef(false) + + // Local search state with debouncing for the header + const [searchQuery, setSearchQuery] = useState(storeSearchQuery) + const debouncedSearchQuery = useDebounce(searchQuery, 300) + + // Live and refresh state + const [isLive, setIsLive] = useState(false) + const [isRefreshing, setIsRefreshing] = useState(false) + const liveIntervalRef = useRef(null) + + // Sync local search query with store search query + useEffect(() => { + setSearchQuery(storeSearchQuery) + }, [storeSearchQuery]) + + // Update store when debounced search query changes + useEffect(() => { + if (debouncedSearchQuery !== storeSearchQuery) { + setStoreSearchQuery(debouncedSearchQuery) + } + }, [debouncedSearchQuery, storeSearchQuery, setStoreSearchQuery]) const handleLogClick = (log: WorkflowLog) => { setSelectedLog(log) @@ -140,15 +187,80 @@ export default function Logs() { [setLogs, setLoading, setError, setHasMore, setIsFetchingMore, buildQueryParams] ) + const handleRefresh = async () => { + if (isRefreshing) return + + setIsRefreshing(true) + + const minLoadingTime = new Promise((resolve) => setTimeout(resolve, 1000)) + + try { + const logsResponse = await fetchLogs(1) + await minLoadingTime + setError(null) + } catch (err) { + await minLoadingTime + setError(err instanceof Error ? err.message : 'An unknown error occurred') + } finally { + setIsRefreshing(false) + } + } + + // Setup or clear the live refresh interval when isLive changes useEffect(() => { - fetchLogs(1) + if (liveIntervalRef.current) { + clearInterval(liveIntervalRef.current) + liveIntervalRef.current = null + } + + if (isLive) { + handleRefresh() + liveIntervalRef.current = setInterval(() => { + handleRefresh() + }, 5000) + } + + return () => { + if (liveIntervalRef.current) { + clearInterval(liveIntervalRef.current) + liveIntervalRef.current = null + } + } + }, [isLive]) + + const toggleLive = () => { + setIsLive(!isLive) + } + + // Initialize filters from URL on mount + useEffect(() => { + if (!isInitialized.current) { + isInitialized.current = true + initializeFromURL() + } + }, [initializeFromURL]) + + // Handle browser navigation events (back/forward) + useEffect(() => { + const handlePopState = () => { + initializeFromURL() + } + + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [initializeFromURL]) + + useEffect(() => { + // Only fetch logs after initialization + if (isInitialized.current) { + fetchLogs(1) + } }, [fetchLogs]) // Refetch when filters change (but not on initial load) - const isInitialMount = useRef(true) useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false + // Only fetch when initialized and filters change + if (!isInitialized.current) { return } @@ -297,34 +409,99 @@ export default function Logs() { ]) return ( -
+
{/* Add the animation styles */} - -
- -
+
+
+ {/* Header */} +
+

+ Logs +

+
+ + {/* Search and Controls */} +
+
+ + setSearchQuery(e.target.value)} + className='flex-1 border-0 bg-transparent px-0 font-[380] font-sans text-base text-foreground leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+ +
+ + + + + {isRefreshing ? 'Refreshing...' : 'Refresh'} + + + +
+
+ {/* Table container */}
- {/* Table with fixed layout */} -
+ {/* Table with responsive layout */} +
{/* Header */} -
-
-
-
Time
-
Status
-
Workflow
-
+
+
+
+
+ Time +
+
+ Status +
+
+ Workflow +
+
+ ID +
+
Trigger
-
- Cost +
+ Message +
+
+ Duration
-
Duration
@@ -354,7 +531,7 @@ export default function Logs() {
) : ( -
+
{logs.map((log) => { const formattedDate = formatDate(log.createdAt) const isSelected = selectedLog?.id === log.id @@ -363,67 +540,85 @@ export default function Logs() {
handleLogClick(log)} > -
+
{/* Time */}
-
{formattedDate.formatted}
-
- {formattedDate.relative} +
+ + {formattedDate.compactDate} + + + {formattedDate.compactTime} +
{/* Status */}
- - {log.level === 'error' ? 'Failed' : 'Success'} - + {log.level}
{/* Workflow */}
-
+
{log.workflow?.name || 'Unknown Workflow'}
-
- {log.message} +
+ + {/* ID */} +
+
+ #{log.id.slice(-4)}
{/* Trigger */} -
-
- {log.trigger || '—'} -
+
+ {log.trigger ? ( +
+ {log.trigger} +
+ ) : ( +
+ )}
- {/* Cost */} -
-
- {log.metadata?.enhanced && log.metadata?.cost?.total ? ( - ${log.metadata.cost.total.toFixed(4)} - ) : ( - - )} -
+ {/* Message */} +
+
{log.message}
{/* Duration */} -
+
{log.duration || '—'}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils/format-date.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils/format-date.ts index 4de785e7c..8463e3f37 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils/format-date.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils/format-date.ts @@ -22,6 +22,9 @@ export const formatDate = (dateString: string) => { hour12: false, }), formatted: format(date, 'HH:mm:ss'), + compact: format(date, 'MMM d HH:mm:ss'), + compactDate: format(date, 'MMM d').toUpperCase(), + compactTime: format(date, 'HH:mm:ss'), relative: (() => { const now = new Date() const diffMs = now.getTime() - date.getTime() diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/logs-filters/logs-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/logs-filters/logs-filters.tsx new file mode 100644 index 000000000..6f8f40fde --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/logs-filters/logs-filters.tsx @@ -0,0 +1,31 @@ +'use client' + +import { ScrollArea } from '@/components/ui/scroll-area' +import FilterSection from '@/app/workspace/[workspaceId]/logs/components/filters/components/filter-section' +import FolderFilter from '@/app/workspace/[workspaceId]/logs/components/filters/components/folder' +import Level from '@/app/workspace/[workspaceId]/logs/components/filters/components/level' +import Timeline from '@/app/workspace/[workspaceId]/logs/components/filters/components/timeline' +import Trigger from '@/app/workspace/[workspaceId]/logs/components/filters/components/trigger' +import Workflow from '@/app/workspace/[workspaceId]/logs/components/filters/components/workflow' + +export function LogsFilters() { + const sections = [ + { key: 'timeline', title: 'Timeline', component: }, + { key: 'level', title: 'Level', component: }, + { key: 'trigger', title: 'Trigger', component: }, + { key: 'folder', title: 'Folder', component: }, + { key: 'workflow', title: 'Workflow', component: }, + ] + + return ( +
+ +
+ {sections.map((section) => ( + + ))} +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 6f17331b5..f8156fbda 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -21,6 +21,7 @@ import { SearchModal } from '../search-modal/search-modal' import { CreateMenu } from './components/create-menu/create-menu' import { FolderTree } from './components/folder-tree/folder-tree' import { HelpModal } from './components/help-modal/help-modal' +import { LogsFilters } from './components/logs-filters/logs-filters' import { SettingsModal } from './components/settings-modal/settings-modal' import { Toolbar } from './components/toolbar/toolbar' import { WorkspaceHeader } from './components/workspace-header/workspace-header' @@ -132,6 +133,13 @@ export function Sidebar() { return workflowPageRegex.test(pathname) }, [pathname]) + // Check if we're on the logs page + const isOnLogsPage = useMemo(() => { + // Pattern: /workspace/[workspaceId]/logs + const logsPageRegex = /^\/workspace\/[^/]+\/logs$/ + return logsPageRegex.test(pathname) + }, [pathname]) + /** * Refresh workspace list without validation logic - used for non-current workspace operations */ @@ -861,6 +869,19 @@ export function Sidebar() { />
+ {/* Floating Logs Filters - Only on logs page */} +
+ +
+ {/* Floating Navigation - Always visible */}
{ + if (typeof window === 'undefined') return new URLSearchParams() + return new URLSearchParams(window.location.search) +} + +const updateURL = (params: URLSearchParams) => { + if (typeof window === 'undefined') return + + const url = new URL(window.location.href) + url.search = params.toString() + window.history.replaceState({}, '', url) +} + +const parseTimeRangeFromURL = (value: string | null): TimeRange => { + switch (value) { + case 'past-30-minutes': + return 'Past 30 minutes' + case 'past-hour': + return 'Past hour' + case 'past-24-hours': + return 'Past 24 hours' + default: + return 'All time' + } +} + +const parseLogLevelFromURL = (value: string | null): LogLevel => { + if (value === 'error' || value === 'info') return value + return 'all' +} + +const parseTriggerArrayFromURL = (value: string | null): TriggerType[] => { + if (!value) return [] + return value + .split(',') + .filter((t): t is TriggerType => ['chat', 'api', 'webhook', 'manual', 'schedule'].includes(t)) +} + +const parseStringArrayFromURL = (value: string | null): string[] => { + if (!value) return [] + return value.split(',').filter(Boolean) +} + +const timeRangeToURL = (timeRange: TimeRange): string => { + switch (timeRange) { + case 'Past 30 minutes': + return 'past-30-minutes' + case 'Past hour': + return 'past-hour' + case 'Past 24 hours': + return 'past-24-hours' + default: + return 'all-time' + } +} export const useFilterStore = create((set, get) => ({ logs: [], @@ -15,6 +72,7 @@ export const useFilterStore = create((set, get) => ({ page: 1, hasMore: true, isFetchingMore: false, + _isInitializing: false, // Internal flag to prevent URL sync during initialization setLogs: (logs, append = false) => { if (append) { @@ -31,16 +89,25 @@ export const useFilterStore = create((set, get) => ({ setTimeRange: (timeRange) => { set({ timeRange }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, setLevel: (level) => { set({ level }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, setWorkflowIds: (workflowIds) => { set({ workflowIds }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, toggleWorkflowId: (workflowId) => { @@ -55,11 +122,17 @@ export const useFilterStore = create((set, get) => ({ set({ workflowIds: currentWorkflowIds }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, setFolderIds: (folderIds) => { set({ folderIds }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, toggleFolderId: (folderId) => { @@ -74,16 +147,25 @@ export const useFilterStore = create((set, get) => ({ set({ folderIds: currentFolderIds }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, setSearchQuery: (searchQuery) => { set({ searchQuery }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, setTriggers: (triggers: TriggerType[]) => { set({ triggers }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, toggleTrigger: (trigger: TriggerType) => { @@ -98,6 +180,9 @@ export const useFilterStore = create((set, get) => ({ set({ triggers: currentTriggers }) get().resetPagination() + if (!get()._isInitializing) { + get().syncWithURL() + } }, setLoading: (loading) => set({ loading }), @@ -112,6 +197,66 @@ export const useFilterStore = create((set, get) => ({ resetPagination: () => set({ page: 1, hasMore: true }), + // URL synchronization methods + initializeFromURL: () => { + // Set initialization flag to prevent URL sync during init + set({ _isInitializing: true }) + + const params = getSearchParams() + + const timeRange = parseTimeRangeFromURL(params.get('timeRange')) + const level = parseLogLevelFromURL(params.get('level')) + const workflowIds = parseStringArrayFromURL(params.get('workflowIds')) + const folderIds = parseStringArrayFromURL(params.get('folderIds')) + const triggers = parseTriggerArrayFromURL(params.get('triggers')) + const searchQuery = params.get('search') || '' + + set({ + timeRange, + level, + workflowIds, + folderIds, + triggers, + searchQuery, + _isInitializing: false, // Clear the flag after initialization + }) + + // Ensure URL reflects the initialized state + get().syncWithURL() + }, + + syncWithURL: () => { + const { timeRange, level, workflowIds, folderIds, triggers, searchQuery } = get() + const params = new URLSearchParams() + + // Only add non-default values to keep URL clean + if (timeRange !== 'All time') { + params.set('timeRange', timeRangeToURL(timeRange)) + } + + if (level !== 'all') { + params.set('level', level) + } + + if (workflowIds.length > 0) { + params.set('workflowIds', workflowIds.join(',')) + } + + if (folderIds.length > 0) { + params.set('folderIds', folderIds.join(',')) + } + + if (triggers.length > 0) { + params.set('triggers', triggers.join(',')) + } + + if (searchQuery.trim()) { + params.set('search', searchQuery.trim()) + } + + updateURL(params) + }, + // Build query parameters for server-side filtering buildQueryParams: (page: number, limit: number) => { const { workspaceId, timeRange, level, workflowIds, folderIds, searchQuery, triggers } = get() diff --git a/apps/sim/app/workspace/[workspaceId]/logs/stores/types.ts b/apps/sim/stores/logs/filters/types.ts similarity index 96% rename from apps/sim/app/workspace/[workspaceId]/logs/stores/types.ts rename to apps/sim/stores/logs/filters/types.ts index 278519b60..4a1bb636d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/stores/types.ts +++ b/apps/sim/stores/logs/filters/types.ts @@ -140,6 +140,9 @@ export interface FilterState { hasMore: boolean isFetchingMore: boolean + // Internal state + _isInitializing: boolean + // Actions setLogs: (logs: WorkflowLog[], append?: boolean) => void setWorkspaceId: (workspaceId: string) => void @@ -159,6 +162,10 @@ export interface FilterState { setIsFetchingMore: (isFetchingMore: boolean) => void resetPagination: () => void + // URL synchronization methods + initializeFromURL: () => void + syncWithURL: () => void + // Build query parameters for server-side filtering buildQueryParams: (page: number, limit: number) => string }