diff --git a/app/w/logs/components/sidebar/sidebar.tsx b/app/w/logs/components/sidebar/sidebar.tsx new file mode 100644 index 000000000..302a38ceb --- /dev/null +++ b/app/w/logs/components/sidebar/sidebar.tsx @@ -0,0 +1,250 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { ScrollArea } from '@/components/ui/scroll-area' +import { WorkflowLog } from '@/app/w/logs/stores/types' +import { formatDate } from '@/app/w/logs/utils/format-date' + +interface LogSidebarProps { + log: WorkflowLog | null + isOpen: boolean + onClose: () => void +} + +/** + * Formats JSON content for display, handling multiple JSON objects separated by '--' + */ +const formatJsonContent = (content: string): JSX.Element => { + // Check if the content has multiple parts separated by '--' + const parts = content.split(/\s*--\s*/g).filter((part) => part.trim().length > 0) + + if (parts.length > 1) { + // Handle multiple parts + return ( +
+ {parts.map((part, index) => ( +
+ {formatSingleJsonContent(part)} +
+ ))} +
+ ) + } + + // Handle single part + return formatSingleJsonContent(content) +} + +/** + * Formats a single JSON content part + */ +const formatSingleJsonContent = (content: string): JSX.Element => { + try { + // Try to parse the content as JSON + const jsonStart = content.indexOf('{') + if (jsonStart === -1) return
{content}
+ + const messagePart = content.substring(0, jsonStart).trim() + const jsonPart = content.substring(jsonStart) + + try { + const jsonData = JSON.parse(jsonPart) + + return ( +
+ {messagePart &&
{messagePart}
} +
+
+              {JSON.stringify(jsonData, null, 2)}
+            
+
+
+ ) + } catch (e) { + // If JSON parsing fails, try to find and format any valid JSON objects in the content + const jsonRegex = /{[^{}]*({[^{}]*})*[^{}]*}/g + const jsonMatches = content.match(jsonRegex) + + if (jsonMatches && jsonMatches.length > 0) { + return ( +
+ {messagePart && ( +
{messagePart}
+ )} + {jsonMatches.map((jsonStr, idx) => { + try { + const parsedJson = JSON.parse(jsonStr) + return ( +
+
+                      {JSON.stringify(parsedJson, null, 2)}
+                    
+
+ ) + } catch { + return ( +
+ {jsonStr} +
+ ) + } + })} +
+ ) + } + } + } catch (e) { + // If all parsing fails, return the original content + } + + return
{content}
+} + +export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) { + const [width, setWidth] = useState(400) // Default width from the original styles + const [isDragging, setIsDragging] = useState(false) + + const formattedContent = useMemo(() => { + if (!log) return null + return formatJsonContent(log.message) + }, [log]) + + const handleMouseDown = (e: React.MouseEvent) => { + setIsDragging(true) + e.preventDefault() + } + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (isDragging) { + const newWidth = window.innerWidth - e.clientX + // Maintain minimum and maximum widths + setWidth(Math.max(400, Math.min(newWidth, window.innerWidth * 0.8))) + } + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + }, [isDragging]) + + // Handle escape key to close the sidebar + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isOpen, onClose]) + + return ( +
+
+ {log && ( + <> + {/* Header */} +
+

Log Details

+ +
+ + {/* Content */} + + {' '} + {/* Adjust for header height */} +
+ {/* Timestamp */} +
+

Timestamp

+

{formatDate(log.createdAt).full}

+
+ + {/* Workflow */} + {log.workflow && ( +
+

Workflow

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

Execution ID

+

{log.executionId}

+
+ )} + + {/* Level */} +
+

Level

+

{log.level}

+
+ + {/* Trigger */} + {log.trigger && ( +
+

Trigger

+

{log.trigger}

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

Duration

+

{log.duration}

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

Message

+
{formattedContent}
+
+
+
+ + )} +
+ ) +} diff --git a/app/w/logs/logs.tsx b/app/w/logs/logs.tsx index af5337526..2a10fd48a 100644 --- a/app/w/logs/logs.tsx +++ b/app/w/logs/logs.tsx @@ -1,52 +1,14 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useMemo, useState } from 'react' import { format } from 'date-fns' -import { AlertCircle, Clock, Info, Loader2 } from 'lucide-react' +import { AlertCircle, Info, Loader2 } from 'lucide-react' import { ControlBar } from './components/control-bar/control-bar' import { Filters } from './components/filters/filters' +import { Sidebar } from './components/sidebar/sidebar' import { useFilterStore } from './stores/store' -import { LogsResponse } from './stores/types' - -// Helper function to format date -const formatDate = (dateString: string) => { - const date = new Date(dateString) - return { - full: date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }), - time: date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }), - formatted: format(date, 'HH:mm:ss'), - relative: (() => { - const now = new Date() - const diffMs = now.getTime() - date.getTime() - const diffMins = Math.floor(diffMs / 60000) - - if (diffMins < 1) return 'just now' - if (diffMins < 60) return `${diffMins}m ago` - - const diffHours = Math.floor(diffMins / 60) - if (diffHours < 24) return `${diffHours}h ago` - - const diffDays = Math.floor(diffHours / 24) - if (diffDays === 1) return 'yesterday' - if (diffDays < 7) return `${diffDays}d ago` - - return format(date, 'MMM d') - })(), - } -} +import { LogsResponse, WorkflowLog } from './stores/types' +import { formatDate } from './utils/format-date' // Helper function to get level badge styling const getLevelBadgeStyles = (level: string) => { @@ -69,6 +31,43 @@ const getTriggerBadgeStyles = (trigger: string) => { export default function Logs() { const { filteredLogs, logs, loading, error, setLogs, setLoading, setError } = useFilterStore() + const [selectedLog, setSelectedLog] = useState(null) + const [isSidebarOpen, setIsSidebarOpen] = useState(false) + + // Group logs by executionId to identify the last log in each group + const executionGroups = useMemo(() => { + const groups: Record = {} + + // Group logs by executionId + filteredLogs.forEach((log) => { + if (log.executionId) { + if (!groups[log.executionId]) { + groups[log.executionId] = [] + } + groups[log.executionId].push(log) + } + }) + + // Sort logs within each group by createdAt + Object.keys(groups).forEach((executionId) => { + groups[executionId].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ) + }) + + return groups + }, [filteredLogs]) + + // Handle log click + const handleLogClick = (log: WorkflowLog) => { + setSelectedLog(log) + setIsSidebarOpen(true) + } + + // Close sidebar + const handleCloseSidebar = () => { + setIsSidebarOpen(false) + } // Fetch logs on component mount useEffect(() => { @@ -160,10 +159,12 @@ export default function Logs() {
{filteredLogs.map((log) => { const formattedDate = formatDate(log.createdAt) + return (
handleLogClick(log)} >
{/* Time column */} @@ -172,7 +173,11 @@ export default function Logs() { {formattedDate.formatted} - {format(new Date(log.createdAt), 'MMM d, yyyy')} + {new Date(log.createdAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })}
@@ -245,6 +250,9 @@ export default function Logs() {
+ + {/* Log Sidebar */} +
) } diff --git a/app/w/logs/utils/format-date.ts b/app/w/logs/utils/format-date.ts new file mode 100644 index 000000000..4de785e7c --- /dev/null +++ b/app/w/logs/utils/format-date.ts @@ -0,0 +1,43 @@ +import { format } from 'date-fns' + +/** + * Helper function to format date in various formats + */ +export const formatDate = (dateString: string) => { + const date = new Date(dateString) + return { + full: date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }), + time: date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }), + formatted: format(date, 'HH:mm:ss'), + relative: (() => { + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffMins = Math.floor(diffMs / 60000) + + if (diffMins < 1) return 'just now' + if (diffMins < 60) return `${diffMins}m ago` + + const diffHours = Math.floor(diffMins / 60) + if (diffHours < 24) return `${diffHours}h ago` + + const diffDays = Math.floor(diffHours / 24) + if (diffDays === 1) return 'yesterday' + if (diffDays < 7) return `${diffDays}d ago` + + return format(date, 'MMM d') + })(), + } +}