improvement: added scrolling navigation to logs + general improvements

This commit is contained in:
Waleed Latif
2025-03-15 15:48:49 -07:00
parent 6215a6e8da
commit bdf123c2bd
2 changed files with 229 additions and 20 deletions
+101 -14
View File
@@ -1,10 +1,11 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { X } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { ChevronDown, ChevronUp, X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { CopyButton } from '@/components/ui/copy-button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { WorkflowLog } from '@/app/w/logs/stores/types'
import { formatDate } from '@/app/w/logs/utils/format-date'
import { ToolCallsDisplay } from '../tool-calls/tool-calls-display'
@@ -14,6 +15,10 @@ interface LogSidebarProps {
log: WorkflowLog | null
isOpen: boolean
onClose: () => void
onNavigateNext?: () => void
onNavigatePrev?: () => void
hasNext?: boolean
hasPrev?: boolean
}
/**
@@ -113,15 +118,39 @@ const formatSingleJsonContent = (content: string): JSX.Element => {
)
}
export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) {
export function Sidebar({
log,
isOpen,
onClose,
onNavigateNext,
onNavigatePrev,
hasNext = false,
hasPrev = false,
}: LogSidebarProps) {
const [width, setWidth] = useState(400) // Default width from the original styles
const [isDragging, setIsDragging] = useState(false)
const [currentLogId, setCurrentLogId] = useState<string | null>(null)
const scrollAreaRef = useRef<HTMLDivElement>(null)
// Update currentLogId when log changes
useEffect(() => {
if (log?.id) {
setCurrentLogId(log.id)
}
}, [log?.id])
const formattedContent = useMemo(() => {
if (!log) return null
return formatJsonContent(log.message)
}, [log])
// Reset scroll position when log changes
useEffect(() => {
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTop = 0
}
}, [log?.id])
// Determine if this is a workflow execution log
const isWorkflowExecutionLog = useMemo(() => {
if (!log) return false
@@ -173,11 +202,31 @@ export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) {
if (e.key === 'Escape' && isOpen) {
onClose()
}
// Add keyboard shortcuts for navigation
if (isOpen) {
// Up arrow key for previous log
if (e.key === 'ArrowUp' && hasPrev && onNavigatePrev) {
e.preventDefault()
handleNavigate(onNavigatePrev)
}
// Down arrow key for next log
if (e.key === 'ArrowDown' && hasNext && onNavigateNext) {
e.preventDefault()
handleNavigate(onNavigateNext)
}
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose])
}, [isOpen, onClose, hasPrev, hasNext, onNavigatePrev, onNavigateNext])
// Handle navigation
const handleNavigate = (navigateFunction: () => void) => {
navigateFunction()
}
return (
<div
@@ -195,19 +244,57 @@ export function Sidebar({ log, isOpen, onClose }: LogSidebarProps) {
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b">
<h2 className="text-base font-medium">Log Details</h2>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={onClose}
aria-label="Close"
>
<X className="h-4 w-4" />
</Button>
<div className="flex items-center space-x-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={() => hasPrev && handleNavigate(onNavigatePrev!)}
disabled={!hasPrev}
aria-label="Previous log"
>
<ChevronUp className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Previous log (↑)</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={() => hasNext && handleNavigate(onNavigateNext!)}
disabled={!hasNext}
aria-label="Next log"
>
<ChevronDown className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Next log (↓)</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 p-0"
onClick={onClose}
aria-label="Close"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* Content */}
<ScrollArea className="h-[calc(100vh-64px-49px)]">
<ScrollArea className="h-[calc(100vh-64px-49px)]" ref={scrollAreaRef}>
{' '}
{/* Adjust for header height */}
<div className="p-4 space-y-4">
+128 -6
View File
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { AlertCircle, Info, Loader2 } from 'lucide-react'
import { createLogger } from '@/lib/logs/console-logger'
import { ControlBar } from './components/control-bar/control-bar'
@@ -31,10 +31,25 @@ const getTriggerBadgeStyles = (trigger: string) => {
: 'bg-blue-100 dark:bg-blue-950/40 text-blue-700 dark:text-blue-400'
}
// Add a new CSS class for the selected row animation
const selectedRowAnimation = `
@keyframes borderPulse {
0% { border-left-color: hsl(var(--primary) / 0.3); }
50% { border-left-color: hsl(var(--primary) / 0.7); }
100% { border-left-color: hsl(var(--primary) / 0.5); }
}
.selected-row {
animation: borderPulse 1s ease-in-out;
border-left-color: hsl(var(--primary) / 0.5);
}
`
export default function Logs() {
const { filteredLogs, logs, loading, error, setLogs, setLoading, setError } = useFilterStore()
const [selectedLog, setSelectedLog] = useState<WorkflowLog | null>(null)
const [selectedLogIndex, setSelectedLogIndex] = useState<number>(-1)
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const selectedRowRef = useRef<HTMLTableRowElement | null>(null)
// Group logs by executionId to identify the last log in each group
const executionGroups = useMemo(() => {
@@ -63,14 +78,45 @@ export default function Logs() {
// Handle log click
const handleLogClick = (log: WorkflowLog) => {
setSelectedLog(log)
// Find the index of the clicked log in the filtered logs array
const index = filteredLogs.findIndex((l) => l.id === log.id)
setSelectedLogIndex(index)
setIsSidebarOpen(true)
}
// Navigate to the next log
const handleNavigateNext = () => {
if (selectedLogIndex < filteredLogs.length - 1) {
const nextIndex = selectedLogIndex + 1
setSelectedLogIndex(nextIndex)
setSelectedLog(filteredLogs[nextIndex])
}
}
// Navigate to the previous log
const handleNavigatePrev = () => {
if (selectedLogIndex > 0) {
const prevIndex = selectedLogIndex - 1
setSelectedLogIndex(prevIndex)
setSelectedLog(filteredLogs[prevIndex])
}
}
// Close sidebar
const handleCloseSidebar = () => {
setIsSidebarOpen(false)
}
// Scroll selected log into view when it changes
useEffect(() => {
if (selectedRowRef.current) {
selectedRowRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
}, [selectedLogIndex])
// Fetch logs on component mount
useEffect(() => {
const fetchLogs = async () => {
@@ -101,8 +147,63 @@ export default function Logs() {
fetchLogs()
}, [setLogs, setLoading, setError])
// Add keyboard navigation for the logs table
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Only handle keyboard navigation if we have logs and a log is selected
if (filteredLogs.length === 0) return
// If no log is selected yet, select the first one on arrow key press
if (selectedLogIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
e.preventDefault()
setSelectedLogIndex(0)
setSelectedLog(filteredLogs[0])
return
}
// Up arrow key for previous log
if (e.key === 'ArrowUp' && !e.metaKey && !e.ctrlKey && selectedLogIndex > 0) {
e.preventDefault()
handleNavigatePrev()
}
// Down arrow key for next log
if (
e.key === 'ArrowDown' &&
!e.metaKey &&
!e.ctrlKey &&
selectedLogIndex < filteredLogs.length - 1
) {
e.preventDefault()
handleNavigateNext()
}
// Enter key to open/close sidebar
if (e.key === 'Enter' && selectedLog) {
e.preventDefault()
setIsSidebarOpen(!isSidebarOpen)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [
filteredLogs,
selectedLogIndex,
isSidebarOpen,
selectedLog,
handleNavigateNext,
handleNavigatePrev,
setIsSidebarOpen,
])
return (
<div className="flex flex-col h-[100vh]">
{/* Add the animation styles */}
<style jsx global>
{selectedRowAnimation}
</style>
<ControlBar />
<div className="flex flex-1 overflow-hidden">
<Filters />
@@ -186,17 +287,25 @@ export default function Logs() {
<tbody>
{filteredLogs.map((log) => {
const formattedDate = formatDate(log.createdAt)
const isSelected = selectedLog?.id === log.id
return (
<tr
key={log.id}
className="border-b hover:bg-accent/30 transition-colors cursor-pointer"
ref={isSelected ? selectedRowRef : null}
className={`border-b transition-colors cursor-pointer ${
isSelected
? 'bg-accent/40 hover:bg-accent/50 border-l-2 selected-row'
: 'hover:bg-accent/30'
}`}
onClick={() => handleLogClick(log)}
>
{/* Time column */}
<td className="px-4 py-3">
<div className="flex flex-col justify-center">
<div className="text-xs font-medium flex items-center">
<div
className={`text-xs font-medium flex items-center ${isSelected ? 'text-foreground' : ''}`}
>
<span>{formattedDate.formatted}</span>
<span className="mx-1.5 text-muted-foreground hidden xl:inline">
•
@@ -260,14 +369,19 @@ export default function Logs() {
{/* Message column */}
<td className="px-4 py-3">
<div className="text-sm truncate" title={log.message}>
<div
className={`text-sm truncate ${isSelected ? 'text-foreground' : ''}`}
title={log.message}
>
{log.message}
</div>
</td>
{/* Duration column */}
<td className="px-4 py-3">
<div className="text-xs text-muted-foreground">
<div
className={`text-xs ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}
>
{log.duration || '—'}
</div>
</td>
@@ -283,7 +397,15 @@ export default function Logs() {
</div>
{/* Log Sidebar */}
<Sidebar log={selectedLog} isOpen={isSidebarOpen} onClose={handleCloseSidebar} />
<Sidebar
log={selectedLog}
isOpen={isSidebarOpen}
onClose={handleCloseSidebar}
onNavigateNext={handleNavigateNext}
onNavigatePrev={handleNavigatePrev}
hasNext={selectedLogIndex < filteredLogs.length - 1}
hasPrev={selectedLogIndex > 0}
/>
</div>
)
}