diff --git a/app/w/logs/components/filters/filter-section/components/level.tsx b/app/w/logs/components/filters/filter-section/components/level.tsx new file mode 100644 index 0000000000..c7a4b37391 --- /dev/null +++ b/app/w/logs/components/filters/filter-section/components/level.tsx @@ -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 ( + + + + + + {levels.map((levelItem) => ( + setLevel(levelItem.value)} + className="flex items-center justify-between p-2 cursor-pointer text-sm" + > +
+ {levelItem.color && ( +
+ )} + {levelItem.label} +
+ {level === levelItem.value && } + + ))} + + + ) +} diff --git a/app/w/logs/components/filters/components/timeline.tsx b/app/w/logs/components/filters/filter-section/components/timeline.tsx similarity index 53% rename from app/w/logs/components/filters/components/timeline.tsx rename to app/w/logs/components/filters/filter-section/components/timeline.tsx index b21d9b9afc..4329bb3d02 100644 --- a/app/w/logs/components/filters/components/timeline.tsx +++ b/app/w/logs/components/filters/filter-section/components/timeline.tsx @@ -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 ( - + {timeRanges.map((range) => ( 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} + {range} + {timeRange === range && } ))} diff --git a/app/w/logs/components/filters/filter-section/components/workflow.tsx b/app/w/logs/components/filters/filter-section/components/workflow.tsx new file mode 100644 index 0000000000..ec7dcb6ae7 --- /dev/null +++ b/app/w/logs/components/filters/filter-section/components/workflow.tsx @@ -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 ( + + + + + + setWorkflowId(null)} + className="flex items-center justify-between p-2 cursor-pointer text-sm" + > + All workflows + {workflowId === null && } + + + {workflows.map((workflow) => ( + setWorkflowId(workflow.id)} + className="flex items-center justify-between p-2 cursor-pointer text-sm" + > +
+
+ {workflow.name} +
+ {workflowId === workflow.id && } + + ))} + + + ) +} diff --git a/app/w/logs/components/filters/components/filter-section/filter-section.tsx b/app/w/logs/components/filters/filter-section/filter-section.tsx similarity index 100% rename from app/w/logs/components/filters/components/filter-section/filter-section.tsx rename to app/w/logs/components/filters/filter-section/filter-section.tsx diff --git a/app/w/logs/components/filters/filters.tsx b/app/w/logs/components/filters/filters.tsx index 976a06e7e5..af0194fd58 100644 --- a/app/w/logs/components/filters/filters.tsx +++ b/app/w/logs/components/filters/filters.tsx @@ -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 */} } /> - {/* Additional filter sections */} - - + {/* Level Filter */} + } /> + + {/* Workflow Filter */} + } />
) } diff --git a/app/w/logs/logs.tsx b/app/w/logs/logs.tsx index 7625072656..88fcb31ef0 100644 --- a/app/w/logs/logs.tsx +++ b/app/w/logs/logs.tsx @@ -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([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(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 (
@@ -153,20 +109,22 @@ export default function Logs() { {/* Table header - fixed */}
-
- +
Time
Status
-
+
Workflow
+
+ id +
Trigger
-
+
Message
@@ -187,14 +145,14 @@ export default function Logs() { Error: {error}
- ) : logs.length === 0 ? ( + ) : filteredLogs.length === 0 ? (
No logs found
) : (
- {logs.map((log) => { + {filteredLogs.map((log) => { const formattedDate = formatDate(log.createdAt) return (
{/* Workflow column */} -
+
{log.workflow && (
- {log.workflow.name} + {log.workflow.name}
)}
+ {/* ID column - hidden on small screens */} +
+
+ {log.executionId ? `#${log.executionId.substring(0, 4)}` : '—'} +
+
+ {/* Trigger column - hidden on medium screens and below */}
{log.trigger && (
- {log.trigger} + {log.trigger}
)}
- {/* Message column - expanded on medium screens when trigger is hidden */} -
+ {/* Message column - smaller on small screens to give space to status and workflow */} +
{log.message}
diff --git a/app/w/logs/stores/store.ts b/app/w/logs/stores/store.ts new file mode 100644 index 0000000000..7ff9af536e --- /dev/null +++ b/app/w/logs/stores/store.ts @@ -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((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 }) + }, +})) diff --git a/app/w/logs/stores/types.ts b/app/w/logs/stores/types.ts new file mode 100644 index 0000000000..f7d02a20d0 --- /dev/null +++ b/app/w/logs/stores/types.ts @@ -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 +}