mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(logs): added filtering by trigger type
This commit is contained in:
@@ -175,6 +175,7 @@ describe('Workflow Logs API Route', () => {
|
||||
id: 'workflowLogs.id',
|
||||
workflowId: 'workflowLogs.workflowId',
|
||||
level: 'workflowLogs.level',
|
||||
trigger: 'workflowLogs.trigger',
|
||||
createdAt: 'workflowLogs.createdAt',
|
||||
message: 'workflowLogs.message',
|
||||
executionId: 'workflowLogs.executionId',
|
||||
@@ -470,5 +471,61 @@ describe('Workflow Logs API Route', () => {
|
||||
expect(data.data).toHaveLength(2)
|
||||
expect(data.data.every((log: any) => log.executionId === 'exec-1')).toBe(true)
|
||||
})
|
||||
|
||||
it('should filter logs by single trigger type', async () => {
|
||||
const apiLogs = mockWorkflowLogs.filter((log) => log.trigger === 'api')
|
||||
setupDatabaseMock({ logs: apiLogs })
|
||||
|
||||
const url = new URL('http://localhost:3000/api/logs?triggers=api')
|
||||
const req = new Request(url.toString())
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.data).toHaveLength(1)
|
||||
expect(data.data[0].trigger).toBe('api')
|
||||
})
|
||||
|
||||
it('should filter logs by multiple trigger types', async () => {
|
||||
const manualAndApiLogs = mockWorkflowLogs.filter(
|
||||
(log) => log.trigger === 'manual' || log.trigger === 'api'
|
||||
)
|
||||
setupDatabaseMock({ logs: manualAndApiLogs })
|
||||
|
||||
const url = new URL('http://localhost:3000/api/logs?triggers=manual,api')
|
||||
const req = new Request(url.toString())
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.data).toHaveLength(3)
|
||||
expect(data.data.every((log: any) => ['manual', 'api'].includes(log.trigger))).toBe(true)
|
||||
})
|
||||
|
||||
it('should combine trigger filter with other filters', async () => {
|
||||
const filteredLogs = mockWorkflowLogs.filter(
|
||||
(log) => log.trigger === 'manual' && log.level === 'info' && log.workflowId === 'workflow-1'
|
||||
)
|
||||
setupDatabaseMock({ logs: filteredLogs })
|
||||
|
||||
const url = new URL(
|
||||
'http://localhost:3000/api/logs?triggers=manual&level=info&workflowIds=workflow-1'
|
||||
)
|
||||
const req = new Request(url.toString())
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req as any)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.data).toHaveLength(1)
|
||||
expect(data.data[0].trigger).toBe('manual')
|
||||
expect(data.data[0].level).toBe('info')
|
||||
expect(data.data[0].workflowId).toBe('workflow-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ const QueryParamsSchema = z.object({
|
||||
offset: z.coerce.number().optional().default(0),
|
||||
level: z.string().optional(),
|
||||
workflowIds: z.string().optional(), // Comma-separated list of workflow IDs
|
||||
triggers: z.string().optional(), // Comma-separated list of trigger types
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
search: z.string().optional(),
|
||||
@@ -79,6 +80,18 @@ export async function GET(request: NextRequest) {
|
||||
conditions = and(conditions, eq(workflowLogs.level, params.level))
|
||||
}
|
||||
|
||||
if (params.triggers) {
|
||||
const triggerTypes = params.triggers.split(',').map((trigger) => trigger.trim())
|
||||
if (triggerTypes.length === 1) {
|
||||
conditions = and(conditions, eq(workflowLogs.trigger, triggerTypes[0]))
|
||||
} else {
|
||||
conditions = and(
|
||||
conditions,
|
||||
or(...triggerTypes.map((trigger) => eq(workflowLogs.trigger, trigger)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (params.startDate) {
|
||||
const startDate = new Date(params.startDate)
|
||||
conditions = and(conditions, gte(workflowLogs.createdAt, startDate))
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Check, ChevronDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useFilterStore } from '@/app/w/logs/stores/store'
|
||||
import type { TriggerType } from '../../../stores/types'
|
||||
|
||||
export default function Trigger() {
|
||||
const { triggers, toggleTrigger, setTriggers } = useFilterStore()
|
||||
const triggerOptions: { value: TriggerType; label: string; color?: string }[] = [
|
||||
{ value: 'manual', label: 'Manual', color: 'bg-secondary' },
|
||||
{ value: 'api', label: 'API', color: 'bg-blue-500' },
|
||||
{ value: 'webhook', label: 'Webhook', color: 'bg-orange-500' },
|
||||
{ value: 'schedule', label: 'Schedule', color: 'bg-green-500' },
|
||||
{ value: 'chat', label: 'Chat', color: 'bg-purple-500' },
|
||||
]
|
||||
|
||||
// Get display text for the dropdown button
|
||||
const getSelectedTriggersText = () => {
|
||||
if (triggers.length === 0) return 'All triggers'
|
||||
if (triggers.length === 1) {
|
||||
const selected = triggerOptions.find((t) => t.value === triggers[0])
|
||||
return selected ? selected.label : 'All triggers'
|
||||
}
|
||||
return `${triggers.length} triggers selected`
|
||||
}
|
||||
|
||||
// Check if a trigger is selected
|
||||
const isTriggerSelected = (trigger: TriggerType) => {
|
||||
return triggers.includes(trigger)
|
||||
}
|
||||
|
||||
// Clear all selections
|
||||
const clearSelections = () => {
|
||||
setTriggers([])
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline' size='sm' className='w-full justify-between font-normal text-sm'>
|
||||
{getSelectedTriggersText()}
|
||||
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start' className='w-[180px]'>
|
||||
<DropdownMenuItem
|
||||
key='all'
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
clearSelections()
|
||||
}}
|
||||
className='flex cursor-pointer items-center justify-between p-2 text-sm'
|
||||
>
|
||||
<span>All triggers</span>
|
||||
{triggers.length === 0 && <Check className='h-4 w-4 text-primary' />}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{triggerOptions.map((triggerItem) => (
|
||||
<DropdownMenuItem
|
||||
key={triggerItem.value}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
toggleTrigger(triggerItem.value)
|
||||
}}
|
||||
className='flex cursor-pointer items-center justify-between p-2 text-sm'
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
{triggerItem.color && (
|
||||
<div className={`mr-2 h-2 w-2 rounded-full ${triggerItem.color}`} />
|
||||
)}
|
||||
{triggerItem.label}
|
||||
</div>
|
||||
{isTriggerSelected(triggerItem.value) && <Check className='h-4 w-4 text-primary' />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useUserSubscription } from '@/hooks/use-user-subscription'
|
||||
import FilterSection from './components/filter-section'
|
||||
import Level from './components/level'
|
||||
import Timeline from './components/timeline'
|
||||
import Trigger from './components/trigger'
|
||||
import Workflow from './components/workflow'
|
||||
|
||||
/**
|
||||
@@ -58,6 +59,9 @@ export function Filters() {
|
||||
{/* Level Filter */}
|
||||
<FilterSection title='Level' defaultOpen={true} content={<Level />} />
|
||||
|
||||
{/* Trigger Filter */}
|
||||
<FilterSection title='Trigger' defaultOpen={true} content={<Trigger />} />
|
||||
|
||||
{/* Workflow Filter */}
|
||||
<FilterSection title='Workflow' defaultOpen={true} content={<Workflow />} />
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ export default function Logs() {
|
||||
level,
|
||||
workflowIds,
|
||||
searchQuery,
|
||||
triggers,
|
||||
} = useFilterStore()
|
||||
|
||||
const [selectedLog, setSelectedLog] = useState<WorkflowLog | null>(null)
|
||||
@@ -225,6 +226,7 @@ export default function Logs() {
|
||||
level,
|
||||
workflowIds,
|
||||
searchQuery,
|
||||
triggers,
|
||||
setPage,
|
||||
setHasMore,
|
||||
setLoading,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import type { FilterState } from './types'
|
||||
import type { FilterState, TriggerType } from './types'
|
||||
|
||||
export const useFilterStore = create<FilterState>((set, get) => ({
|
||||
logs: [],
|
||||
@@ -7,6 +7,7 @@ export const useFilterStore = create<FilterState>((set, get) => ({
|
||||
level: 'all',
|
||||
workflowIds: [],
|
||||
searchQuery: '',
|
||||
triggers: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
page: 1,
|
||||
@@ -57,6 +58,25 @@ export const useFilterStore = create<FilterState>((set, get) => ({
|
||||
get().resetPagination()
|
||||
},
|
||||
|
||||
setTriggers: (triggers: TriggerType[]) => {
|
||||
set({ triggers })
|
||||
get().resetPagination()
|
||||
},
|
||||
|
||||
toggleTrigger: (trigger: TriggerType) => {
|
||||
const currentTriggers = [...get().triggers]
|
||||
const index = currentTriggers.indexOf(trigger)
|
||||
|
||||
if (index === -1) {
|
||||
currentTriggers.push(trigger)
|
||||
} else {
|
||||
currentTriggers.splice(index, 1)
|
||||
}
|
||||
|
||||
set({ triggers: currentTriggers })
|
||||
get().resetPagination()
|
||||
},
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
|
||||
setError: (error) => set({ error }),
|
||||
@@ -71,7 +91,7 @@ export const useFilterStore = create<FilterState>((set, get) => ({
|
||||
|
||||
// Build query parameters for server-side filtering
|
||||
buildQueryParams: (page: number, limit: number) => {
|
||||
const { timeRange, level, workflowIds, searchQuery } = get()
|
||||
const { timeRange, level, workflowIds, searchQuery, triggers } = get()
|
||||
const params = new URLSearchParams()
|
||||
|
||||
params.set('includeWorkflow', 'true')
|
||||
@@ -83,6 +103,11 @@ export const useFilterStore = create<FilterState>((set, get) => ({
|
||||
params.set('level', level)
|
||||
}
|
||||
|
||||
// Add trigger filter
|
||||
if (triggers.length > 0) {
|
||||
params.set('triggers', triggers.join(','))
|
||||
}
|
||||
|
||||
// Add workflow filter
|
||||
if (workflowIds.length > 0) {
|
||||
params.set('workflowIds', workflowIds.join(','))
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface LogsResponse {
|
||||
|
||||
export type TimeRange = 'Past 30 minutes' | 'Past hour' | 'Past 24 hours' | 'All time'
|
||||
export type LogLevel = 'error' | 'info' | 'all'
|
||||
export type TriggerType = 'chat' | 'api' | 'webhook' | 'manual' | 'schedule' | 'all'
|
||||
|
||||
export interface FilterState {
|
||||
// Original logs from API
|
||||
@@ -93,6 +94,7 @@ export interface FilterState {
|
||||
level: LogLevel
|
||||
workflowIds: string[]
|
||||
searchQuery: string
|
||||
triggers: TriggerType[]
|
||||
|
||||
// Loading state
|
||||
loading: boolean
|
||||
@@ -110,6 +112,8 @@ export interface FilterState {
|
||||
setWorkflowIds: (workflowIds: string[]) => void
|
||||
toggleWorkflowId: (workflowId: string) => void
|
||||
setSearchQuery: (query: string) => void
|
||||
setTriggers: (triggers: TriggerType[]) => void
|
||||
toggleTrigger: (trigger: TriggerType) => void
|
||||
setLoading: (loading: boolean) => void
|
||||
setError: (error: string | null) => void
|
||||
setPage: (page: number) => void
|
||||
|
||||
Reference in New Issue
Block a user