mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(logs): completed filters
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
+10
-9
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
},
|
||||
}))
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user