feat(logs): completed filters

This commit is contained in:
Emir Karabeg
2025-03-05 21:13:57 -08:00
parent 8851beea1b
commit bbea7b6afe
8 changed files with 302 additions and 87 deletions
@@ -0,0 +1,51 @@
import { Check, ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { LogLevel, useFilterStore } from '@/app/w/logs/stores/store'
export default function Level() {
const { level, setLevel } = useFilterStore()
const levels: { value: LogLevel; label: string; color?: string }[] = [
{ value: 'all', label: 'Any status' },
{ 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)
return selected ? selected.label : 'Any status'
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between text-sm font-normal">
{getDisplayLabel()}
<ChevronDown className="h-4 w-4 ml-2 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[180px]">
{levels.map((levelItem) => (
<DropdownMenuItem
key={levelItem.value}
onClick={() => setLevel(levelItem.value)}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<div className="flex items-center">
{levelItem.color && (
<div className={`w-2 h-2 rounded-full mr-2 ${levelItem.color}`} />
)}
{levelItem.label}
</div>
{level === levelItem.value && <Check className="h-4 w-4 text-primary" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,5 +1,4 @@
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { Check, ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
@@ -7,27 +6,29 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { TimeRange, useFilterStore } from '@/app/w/logs/stores/store'
export default function Timeline() {
const [selectedTimeRange, setSelectedTimeRange] = useState('Past 30 minutes')
const timeRanges = ['Past 30 minutes', 'Past hour', 'Past 24 hours', 'All time']
const { timeRange, setTimeRange } = useFilterStore()
const timeRanges: TimeRange[] = ['All time', 'Past 30 minutes', 'Past hour', 'Past 24 hours']
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between text-sm font-normal">
{selectedTimeRange}
{timeRange}
<ChevronDown className="h-4 w-4 ml-2 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="overflow-y-auto">
<DropdownMenuContent align="start" className="w-[180px]">
{timeRanges.map((range) => (
<DropdownMenuItem
key={range}
onClick={() => setSelectedTimeRange(range)}
className="flex items-start p-2 cursor-pointer text-sm"
onClick={() => setTimeRange(range)}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
{range}
<span>{range}</span>
{timeRange === range && <Check className="h-4 w-4 text-primary" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
@@ -0,0 +1,75 @@
import { useMemo } from 'react'
import { Check, ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useFilterStore } from '@/app/w/logs/stores/store'
export default function Workflow() {
const { logs, workflowId, setWorkflowId } = useFilterStore()
// Extract unique workflows from logs
const workflows = useMemo(() => {
const uniqueWorkflows = new Map()
logs.forEach((log) => {
if (log.workflow && !uniqueWorkflows.has(log.workflowId)) {
uniqueWorkflows.set(log.workflowId, {
id: log.workflowId,
name: log.workflow.name,
color: log.workflow.color,
})
}
})
return Array.from(uniqueWorkflows.values())
}, [logs])
const getSelectedWorkflowName = () => {
if (!workflowId) return 'All workflows'
const selected = workflows.find((w) => w.id === workflowId)
return selected ? selected.name : 'All workflows'
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between text-sm font-normal">
{getSelectedWorkflowName()}
<ChevronDown className="h-4 w-4 ml-2 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[180px] max-h-[300px] overflow-y-auto">
<DropdownMenuItem
key="all"
onClick={() => setWorkflowId(null)}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<span>All workflows</span>
{workflowId === null && <Check className="h-4 w-4 text-primary" />}
</DropdownMenuItem>
{workflows.map((workflow) => (
<DropdownMenuItem
key={workflow.id}
onClick={() => setWorkflowId(workflow.id)}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<div className="flex items-center">
<div
className="w-2 h-2 rounded-full mr-2"
style={{ backgroundColor: workflow.color }}
/>
{workflow.name}
</div>
{workflowId === workflow.id && <Check className="h-4 w-4 text-primary" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
+9 -15
View File
@@ -1,17 +1,9 @@
'use client'
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import FilterSection from './components/filter-section/filter-section'
import Timeline from './components/timeline'
import Level from './filter-section/components/level'
import Timeline from './filter-section/components/timeline'
import Workflow from './filter-section/components/workflow'
import FilterSection from './filter-section/filter-section'
/**
* Filters component for logs page - includes timeline and other filter options
@@ -24,9 +16,11 @@ export function Filters() {
{/* Timeline Filter */}
<FilterSection title="Timeline" defaultOpen={true} content={<Timeline />} />
{/* Additional filter sections */}
<FilterSection title="Level" />
<FilterSection title="Workflow" />
{/* Level Filter */}
<FilterSection title="Level" defaultOpen={true} content={<Level />} />
{/* Workflow Filter */}
<FilterSection title="Workflow" defaultOpen={true} content={<Workflow />} />
</div>
)
}
+28 -63
View File
@@ -1,40 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import { format } from 'date-fns'
import { AlertCircle, AlertTriangle, ArrowDownUp, Clock, Info, Loader2 } from 'lucide-react'
import { AlertCircle, Clock, Info, Loader2 } from 'lucide-react'
import { ControlBar } from './components/control-bar/control-bar'
import { Filters } from './components/filters/filters'
// Define types for our logs data
interface WorkflowLog {
id: string
workflowId: string
executionId: string | null
level: string
message: string
duration: string | null
trigger: string | null
createdAt: string
workflow?: WorkflowData | null
}
interface WorkflowData {
id: string
name: string
description: string | null
color: string
state: any
// Add other workflow fields as needed
}
interface LogsResponse {
data: WorkflowLog[]
total: number
page: number
pageSize: number
totalPages: number
}
import { useFilterStore } from './stores/store'
import { LogsResponse } from './stores/types'
// Helper function to format date
const formatDate = (dateString: string) => {
@@ -84,7 +56,7 @@ const getLevelBadgeStyles = (level: string) => {
case 'warn':
return 'bg-warning/20 text-warning'
default:
return 'bg-[#7F2FFF]/20 text-[#7F2FFF]'
return 'bg-secondary text-secondary-foreground'
}
}
@@ -96,15 +68,7 @@ const getTriggerBadgeStyles = (trigger: string) => {
}
export default function Logs() {
const [logs, setLogs] = useState<WorkflowLog[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [pagination, setPagination] = useState({
total: 0,
page: 1,
pageSize: 100,
totalPages: 0,
})
const { filteredLogs, logs, loading, error, setLogs, setLoading, setError } = useFilterStore()
// Fetch logs on component mount
useEffect(() => {
@@ -120,29 +84,21 @@ export default function Logs() {
const data: LogsResponse = await response.json()
// Log the response to console as requested
// Log the response to console
console.log('Workflow logs response:', data)
setLogs(data.data)
setPagination({
total: data.total,
page: data.page,
pageSize: data.pageSize,
totalPages: data.totalPages,
})
setError(null)
} catch (err) {
console.error('Failed to fetch logs:', err)
setError(err instanceof Error ? err.message : 'An unknown error occurred')
setLogs([])
} finally {
setLoading(false)
}
}
fetchLogs()
}, [])
}, [setLogs, setLoading, setError])
return (
<div className="flex flex-col h-[100vh]">
@@ -153,20 +109,22 @@ export default function Logs() {
{/* Table header - fixed */}
<div className="border-b bg-background z-10 sticky top-0">
<div className="grid grid-cols-12 gap-4 px-4 py-3 text-xs font-medium text-muted-foreground">
<div className="col-span-2 flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" />
<div className="col-span-2 flex items-center lg:gap-1.5">
<span>Time</span>
</div>
<div className="col-span-1 flex items-center gap-1.5">
<span>Status</span>
</div>
<div className="col-span-2 flex items-center gap-1.5">
<div className="col-span-3 md:col-span-2 flex items-center gap-1.5">
<span>Workflow</span>
</div>
<div className="col-span-1 hidden lg:flex items-center gap-1.5">
<span>id</span>
</div>
<div className="col-span-1 hidden lg:flex items-center gap-1.5">
<span>Trigger</span>
</div>
<div className="col-span-6 md:col-span-5 flex items-center gap-1.5">
<div className="col-span-5 md:col-span-5 lg:col-span-4 flex items-center gap-1.5">
<span>Message</span>
</div>
<div className="col-span-1 flex items-center gap-1.5">
@@ -187,14 +145,14 @@ export default function Logs() {
<AlertCircle className="h-5 w-5 text-destructive" />
<span className="ml-2 text-sm text-destructive">Error: {error}</span>
</div>
) : logs.length === 0 ? (
) : filteredLogs.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<Info className="h-5 w-5 mr-2" />
<span className="text-sm">No logs found</span>
</div>
) : (
<div>
{logs.map((log) => {
{filteredLogs.map((log) => {
const formattedDate = formatDate(log.createdAt)
return (
<div
@@ -226,7 +184,7 @@ export default function Logs() {
</div>
{/* Workflow column */}
<div className="col-span-2 flex items-center">
<div className="col-span-3 md:col-span-2 flex items-center">
{log.workflow && (
<div
className="inline-flex items-center px-2 py-1 text-xs rounded-md truncate max-w-full"
@@ -236,24 +194,31 @@ export default function Logs() {
}}
title={log.workflow.name}
>
{log.workflow.name}
<span className="font-base truncate">{log.workflow.name}</span>
</div>
)}
</div>
{/* ID column - hidden on small screens */}
<div className="col-span-1 hidden lg:flex items-center">
<div className="text-xs font-mono text-muted-foreground">
{log.executionId ? `#${log.executionId.substring(0, 4)}` : '—'}
</div>
</div>
{/* Trigger column - hidden on medium screens and below */}
<div className="col-span-1 hidden lg:flex items-center">
{log.trigger && (
<div
className={`inline-flex items-center px-2 py-1 text-xs rounded-md ${getTriggerBadgeStyles(log.trigger)}`}
>
{log.trigger}
<span className="font-medium">{log.trigger}</span>
</div>
)}
</div>
{/* Message column - expanded on medium screens when trigger is hidden */}
<div className="col-span-6 md:col-span-5 flex items-center">
{/* Message column - smaller on small screens to give space to status and workflow */}
<div className="col-span-5 md:col-span-5 lg:col-span-4 flex items-center">
<div className="text-sm truncate" title={log.message}>
{log.message}
</div>
+101
View File
@@ -0,0 +1,101 @@
import { create } from 'zustand'
import { WorkflowLog } from './types'
export type TimeRange = 'Past 30 minutes' | 'Past hour' | 'Past 24 hours' | 'All time'
export type LogLevel = 'error' | 'info' | 'all'
interface FilterState {
// Original logs from API
logs: WorkflowLog[]
// Filtered logs to display
filteredLogs: WorkflowLog[]
// Filter states
timeRange: TimeRange
level: LogLevel
workflowId: string | null
// Loading state
loading: boolean
error: string | null
// Actions
setLogs: (logs: WorkflowLog[]) => void
setTimeRange: (timeRange: TimeRange) => void
setLevel: (level: LogLevel) => void
setWorkflowId: (workflowId: string | null) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
// Apply filters
applyFilters: () => void
}
export const useFilterStore = create<FilterState>((set, get) => ({
logs: [],
filteredLogs: [],
timeRange: 'All time',
level: 'all',
workflowId: null,
loading: true,
error: null,
setLogs: (logs) => {
set({ logs, filteredLogs: logs, loading: false })
},
setTimeRange: (timeRange) => {
set({ timeRange })
get().applyFilters()
},
setLevel: (level) => {
set({ level })
get().applyFilters()
},
setWorkflowId: (workflowId) => {
set({ workflowId })
get().applyFilters()
},
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
applyFilters: () => {
const { logs, timeRange, level, workflowId } = get()
let filtered = [...logs]
// Apply time range filter
if (timeRange !== 'All time') {
const now = new Date()
let cutoffTime: Date
switch (timeRange) {
case 'Past 30 minutes':
cutoffTime = new Date(now.getTime() - 30 * 60 * 1000)
break
case 'Past hour':
cutoffTime = new Date(now.getTime() - 60 * 60 * 1000)
break
case 'Past 24 hours':
cutoffTime = new Date(now.getTime() - 24 * 60 * 60 * 1000)
break
default:
cutoffTime = new Date(0) // Beginning of time
}
filtered = filtered.filter((log) => new Date(log.createdAt) >= cutoffTime)
}
// Apply level filter
if (level !== 'all') {
filtered = filtered.filter((log) => log.level.toLowerCase() === level)
}
// Apply workflow filter
if (workflowId) {
filtered = filtered.filter((log) => log.workflowId === workflowId)
}
set({ filteredLogs: filtered })
},
}))
+28
View File
@@ -0,0 +1,28 @@
export interface WorkflowData {
id: string
name: string
description: string | null
color: string
state: any
// Add other workflow fields as needed
}
export interface WorkflowLog {
id: string
workflowId: string
executionId: string | null
level: string
message: string
duration: string | null
trigger: string | null
createdAt: string
workflow?: WorkflowData | null
}
export interface LogsResponse {
data: WorkflowLog[]
total: number
page: number
pageSize: number
totalPages: number
}