improvement(logs): surface integration triggers in logs instead of catchall 'webhook' trigger type (#2102)

* improvement(logs): surface integration triggers in logs instead of  catchall

* optimized calculation, added new triggers to search

* cleanup

* fix console log
This commit is contained in:
Waleed
2025-11-22 14:18:42 -08:00
committed by GitHub
parent 33ac828d3d
commit d1f0d21e7f
11 changed files with 424 additions and 131 deletions

View File

@@ -0,0 +1,91 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { and, eq, isNotNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('TriggersAPI')
export const revalidate = 0
const QueryParamsSchema = z.object({
workspaceId: z.string(),
})
/**
* GET /api/logs/triggers
*
* Returns unique trigger types from workflow execution logs
* Only includes integration triggers (excludes core types: api, manual, webhook, chat, schedule)
*/
export async function GET(request: NextRequest) {
const requestId = generateRequestId()
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized triggers access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
try {
const { searchParams } = new URL(request.url)
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
const triggers = await db
.selectDistinct({
trigger: workflowExecutionLogs.trigger,
})
.from(workflowExecutionLogs)
.innerJoin(
workflow,
and(
eq(workflowExecutionLogs.workflowId, workflow.id),
eq(workflow.workspaceId, params.workspaceId)
)
)
.innerJoin(
permissions,
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workflow.workspaceId),
eq(permissions.userId, userId)
)
)
.where(
and(
isNotNull(workflowExecutionLogs.trigger),
sql`${workflowExecutionLogs.trigger} NOT IN ('api', 'manual', 'webhook', 'chat', 'schedule')`
)
)
const triggerValues = triggers
.map((row) => row.trigger)
.filter((t): t is string => Boolean(t))
.sort()
return NextResponse.json({
triggers: triggerValues,
count: triggerValues.length,
})
} catch (err) {
if (err instanceof z.ZodError) {
logger.error(`[${requestId}] Invalid query parameters`, { error: err })
return NextResponse.json(
{ error: 'Invalid query parameters', details: err.errors },
{ status: 400 }
)
}
throw err
}
} catch (err) {
logger.error(`[${requestId}] Failed to fetch triggers`, { error: err })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -14,6 +14,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
import {
commandListClass,
dropdownContentClass,
@@ -26,15 +27,9 @@ import type { TriggerType } from '@/stores/logs/filters/types'
export default function Trigger() {
const { triggers, toggleTrigger, setTriggers } = useFilterStore()
const [search, setSearch] = useState('')
const triggerOptions: { value: TriggerType; label: string; color?: string }[] = [
{ value: 'manual', label: 'Manual', color: 'bg-gray-500' },
{ 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 triggerOptions = useMemo(() => getTriggerOptions(), [])
const getSelectedTriggersText = () => {
if (triggers.length === 0) return 'All triggers'
if (triggers.length === 1) {
@@ -44,12 +39,10 @@ export default function Trigger() {
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([])
}
@@ -98,7 +91,10 @@ export default function Trigger() {
>
<div className='flex items-center'>
{triggerItem.color && (
<div className={`mr-2 h-2 w-2 rounded-full ${triggerItem.color}`} />
<div
className='mr-2 h-2 w-2 rounded-full'
style={{ backgroundColor: triggerItem.color }}
/>
)}
{triggerItem.label}
</div>

View File

@@ -2,11 +2,15 @@
import { useEffect, useMemo, useState } from 'react'
import { Search, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn'
import { createLogger } from '@/lib/logs/console/logger'
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { type ParsedFilter, parseQuery } from '@/lib/logs/query-parser'
import {
type FolderData,
SearchSuggestions,
type TriggerData,
type WorkflowData,
} from '@/lib/logs/search-suggestions'
import { cn } from '@/lib/utils'
@@ -14,6 +18,8 @@ import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-sea
import { useFolderStore } from '@/stores/folders/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('AutocompleteSearch')
interface AutocompleteSearchProps {
value: string
onChange: (value: string) => void
@@ -29,8 +35,11 @@ export function AutocompleteSearch({
className,
onOpenChange,
}: AutocompleteSearchProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const workflows = useWorkflowRegistry((state) => state.workflows)
const folders = useFolderStore((state) => state.folders)
const [triggersData, setTriggersData] = useState<TriggerData[]>([])
const workflowsData = useMemo<WorkflowData[]>(() => {
return Object.values(workflows).map((w) => ({
@@ -47,9 +56,36 @@ export function AutocompleteSearch({
}))
}, [folders])
useEffect(() => {
if (!workspaceId) return
const fetchTriggers = async () => {
try {
const response = await fetch(`/api/logs/triggers?workspaceId=${workspaceId}`)
if (!response.ok) return
const data = await response.json()
const triggers: TriggerData[] = data.triggers.map((trigger: string) => {
const metadata = getIntegrationMetadata(trigger)
return {
value: trigger,
label: metadata.label,
color: metadata.color,
}
})
setTriggersData(triggers)
} catch (error) {
logger.error('Failed to fetch triggers:', error)
}
}
fetchTriggers()
}, [workspaceId])
const suggestionEngine = useMemo(() => {
return new SearchSuggestions(workflowsData, foldersData)
}, [workflowsData, foldersData])
return new SearchSuggestions(workflowsData, foldersData, triggersData)
}, [workflowsData, foldersData, triggersData])
const handleFiltersChange = (filters: ParsedFilter[], textSearch: string) => {
const filterStrings = filters.map(
@@ -84,7 +120,6 @@ export function AutocompleteSearch({
getSuggestions: (input) => suggestionEngine.getSuggestions(input),
})
// Initialize from external value (URL params) - only on mount
useEffect(() => {
if (value) {
const parsed = parseQuery(value)
@@ -269,8 +304,16 @@ export function AutocompleteSearch({
}}
>
<div className='flex items-center justify-between gap-3'>
<div className='min-w-0 flex-1 truncate text-[13px]'>
{suggestion.label}
<div className='flex min-w-0 flex-1 items-center gap-2'>
{suggestion.category === 'trigger' && suggestion.color && (
<div
className='h-2 w-2 flex-shrink-0 rounded-full'
style={{ backgroundColor: suggestion.color }}
/>
)}
<div className='min-w-0 flex-1 truncate text-[13px]'>
{suggestion.label}
</div>
</div>
{suggestion.value !== suggestion.label && (
<div className='flex-shrink-0 font-mono text-[11px] text-[var(--text-muted)]'>
@@ -313,7 +356,15 @@ export function AutocompleteSearch({
}}
>
<div className='flex items-center justify-between gap-3'>
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
<div className='flex min-w-0 flex-1 items-center gap-2'>
{suggestion.category === 'trigger' && suggestion.color && (
<div
className='h-2 w-2 flex-shrink-0 rounded-full'
style={{ backgroundColor: suggestion.color }}
/>
)}
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
</div>
{suggestion.description && (
<div className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
{suggestion.value}

View File

@@ -10,6 +10,7 @@ import { Button, Tooltip } from '@/components/emcn'
import { CopyButton } from '@/components/ui/copy-button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { FrozenCanvasModal } from '@/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas-modal'
import { FileDownload } from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/file-download'
import LogMarkdownRenderer from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer'
@@ -52,49 +53,6 @@ const tryPrettifyJson = (content: string): { isJson: boolean; formatted: string
}
}
/**
* Formats JSON content for display, handling multiple JSON objects separated by '--'
*/
const formatJsonContent = (content: string, blockInput?: Record<string, any>): React.ReactNode => {
const blockPattern = /^(Block .+?\(.+?\):)\s*/
const match = content.match(blockPattern)
if (match) {
const systemComment = match[1]
const actualContent = content.substring(match[0].length).trim()
const { isJson, formatted } = tryPrettifyJson(actualContent)
return (
<BlockContentDisplay
systemComment={systemComment}
formatted={formatted}
isJson={isJson}
blockInput={blockInput}
/>
)
}
const { isJson, formatted } = tryPrettifyJson(content)
return (
<div className='group relative w-full rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F] p-3'>
<CopyButton text={formatted} className='z-10 h-7 w-7' />
{isJson ? (
<div className='code-editor-theme'>
<pre
className='max-h-[500px] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-all font-mono text-[#eeeeee] text-[11px] leading-[16px]'
dangerouslySetInnerHTML={{
__html: highlight(formatted, languages.json, 'json'),
}}
/>
</div>
) : (
<LogMarkdownRenderer content={formatted} />
)}
</div>
)
}
const BlockContentDisplay = ({
systemComment,
formatted,
@@ -212,11 +170,9 @@ export function Sidebar({
const [isFrozenCanvasOpen, setIsFrozenCanvasOpen] = useState(false)
const scrollAreaRef = useRef<HTMLDivElement>(null)
// Update currentLogId when log changes
useEffect(() => {
if (log?.id) {
setCurrentLogId(log.id)
// Reset trace expanded state when log changes
setIsTraceExpanded(false)
}
}, [log?.id])
@@ -260,6 +216,11 @@ export function Sidebar({
return isWorkflowExecutionLog && hasCostInfo
}, [isWorkflowExecutionLog, hasCostInfo])
const triggerMetadata = useMemo(
() => (log?.trigger ? getIntegrationMetadata(log.trigger) : null),
[log?.trigger]
)
const handleTraceSpanToggle = (expanded: boolean) => {
setIsTraceExpanded(expanded)
@@ -465,14 +426,14 @@ export function Sidebar({
</div>
{/* Trigger */}
{log.trigger && (
{log.trigger && triggerMetadata && (
<div>
<h3 className='mb-[4px] font-medium text-[12px] text-[var(--text-tertiary)] dark:text-[var(--text-tertiary)]'>
Trigger
</h3>
<div className='group relative text-[13px] capitalize'>
<div className='group relative text-[13px]'>
<CopyButton text={log.trigger} />
{log.trigger}
{triggerMetadata.label}
</div>
</div>
)}

View File

@@ -1,9 +1,10 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AlertCircle, ArrowUpRight, Info, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
import { cn } from '@/lib/utils'
import Controls from '@/app/workspace/[workspaceId]/logs/components/dashboard/controls'
@@ -20,31 +21,6 @@ import type { WorkflowLog } from '@/stores/logs/filters/types'
const LOGS_PER_PAGE = 50
/**
* Returns the background color for a trigger type badge.
*
* @param trigger - The trigger type (manual, schedule, webhook, chat, api)
* @returns Hex color code for the trigger type
*/
const getTriggerColor = (trigger: string | null | undefined): string => {
if (!trigger) return '#9ca3af'
switch (trigger.toLowerCase()) {
case 'manual':
return '#9ca3af' // gray-400 (matches secondary styling better)
case 'schedule':
return '#10b981' // green (emerald-500)
case 'webhook':
return '#f97316' // orange (orange-500)
case 'chat':
return '#8b5cf6' // purple (violet-500)
case 'api':
return '#3b82f6' // blue (blue-500)
default:
return '#9ca3af' // gray-400
}
}
const selectedRowAnimation = `
@keyframes borderPulse {
0% { border-left-color: hsl(var(--primary) / 0.3) }
@@ -57,6 +33,20 @@ const selectedRowAnimation = `
}
`
const TriggerBadge = React.memo(({ trigger }: { trigger: string }) => {
const metadata = getIntegrationMetadata(trigger)
return (
<div
className='inline-flex items-center rounded-[6px] px-[8px] py-[2px] font-medium text-[12px] text-white'
style={{ backgroundColor: metadata.color }}
>
{metadata.label}
</div>
)
})
TriggerBadge.displayName = 'TriggerBadge'
export default function Logs() {
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -537,12 +527,7 @@ export default function Logs() {
{/* Trigger */}
<div className='hidden xl:block'>
{log.trigger ? (
<div
className='inline-flex items-center rounded-[6px] px-[8px] py-[2px] font-medium text-[12px] text-white'
style={{ backgroundColor: getTriggerColor(log.trigger) }}
>
{log.trigger}
</div>
<TriggerBadge trigger={log.trigger} />
) : (
<div className='font-medium text-[12px] text-[var(--text-secondary)] dark:text-[var(--text-secondary)]'>

View File

@@ -3,6 +3,7 @@ export interface Suggestion {
value: string
label: string
description?: string
color?: string
category?:
| 'filters'
| 'level'

View File

@@ -3,13 +3,11 @@ import { webhook, workflow as workflowTable } from '@sim/db/schema'
import { task } from '@trigger.dev/sdk'
import { eq } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { processExecutionFiles } from '@/lib/execution/files'
import { IdempotencyService, webhookIdempotency } from '@/lib/idempotency'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { decryptSecret } from '@/lib/utils'
import { WebhookAttachmentProcessor } from '@/lib/webhooks/attachment-processor'
import { fetchAndProcessAirtablePayloads, formatWebhookInput } from '@/lib/webhooks/utils.server'
import {
@@ -133,7 +131,12 @@ async function executeWebhookJobInternal(
executionId: string,
requestId: string
) {
const loggingSession = new LoggingSession(payload.workflowId, executionId, 'webhook', requestId)
const loggingSession = new LoggingSession(
payload.workflowId,
executionId,
payload.provider || 'webhook',
requestId
)
try {
const workflowData =
@@ -156,19 +159,6 @@ async function executeWebhookJobInternal(
const workspaceId = wfRows[0]?.workspaceId || undefined
const workflowVariables = (wfRows[0]?.variables as Record<string, any>) || {}
const { personalEncrypted, workspaceEncrypted } = await getPersonalAndWorkspaceEnv(
payload.userId,
workspaceId
)
const mergedEncrypted = { ...personalEncrypted, ...workspaceEncrypted }
const decryptedPairs = await Promise.all(
Object.entries(mergedEncrypted).map(async ([key, encrypted]) => {
const { decrypted } = await decryptSecret(encrypted)
return [key, decrypted] as const
})
)
const decryptedEnvVars: Record<string, string> = Object.fromEntries(decryptedPairs)
// Merge subblock states (matching workflow-execution pattern)
const mergedStates = mergeSubblockState(blocks, {})
@@ -232,7 +222,7 @@ async function executeWebhookJobInternal(
workflowId: payload.workflowId,
workspaceId,
userId: payload.userId,
triggerType: 'webhook',
triggerType: payload.provider || 'webhook',
triggerBlockId: payload.blockId,
useDraftState: false,
startTime: new Date().toISOString(),
@@ -445,7 +435,7 @@ async function executeWebhookJobInternal(
workflowId: payload.workflowId,
workspaceId,
userId: payload.userId,
triggerType: 'webhook',
triggerType: payload.provider || 'webhook',
triggerBlockId: payload.blockId,
useDraftState: false,
startTime: new Date().toISOString(),

View File

@@ -0,0 +1,117 @@
import { getBlock } from '@/blocks/registry'
import { getAllTriggers } from '@/triggers'
export interface TriggerOption {
value: string
label: string
color: string
}
let cachedTriggerOptions: TriggerOption[] | null = null
let cachedTriggerMetadataMap: Map<string, { label: string; color: string }> | null = null
/**
* Reset cache - useful for HMR in development or testing
*/
export function resetTriggerOptionsCache() {
cachedTriggerOptions = null
cachedTriggerMetadataMap = null
}
/**
* Dynamically generates trigger filter options from the trigger registry and block definitions.
* Results are cached after first call for performance (~98% faster on subsequent calls).
*/
export function getTriggerOptions(): TriggerOption[] {
if (cachedTriggerOptions) {
return cachedTriggerOptions
}
const triggers = getAllTriggers()
const providerMap = new Map<string, TriggerOption>()
const coreTypes: TriggerOption[] = [
{ value: 'manual', label: 'Manual', color: '#6b7280' }, // gray-500
{ value: 'api', label: 'API', color: '#3b82f6' }, // blue-500
{ value: 'schedule', label: 'Schedule', color: '#10b981' }, // green-500
{ value: 'chat', label: 'Chat', color: '#8b5cf6' }, // purple-500
{ value: 'webhook', label: 'Webhook', color: '#f97316' }, // orange-500 (for backward compatibility)
]
for (const trigger of triggers) {
const provider = trigger.provider
if (!provider || providerMap.has(provider)) {
continue
}
const block = getBlock(provider)
if (block) {
providerMap.set(provider, {
value: provider,
label: block.name, // Use block's display name (e.g., "Slack", "GitHub")
color: block.bgColor || '#6b7280', // Use block's hex color, fallback to gray
})
} else {
const label = formatProviderName(provider)
providerMap.set(provider, {
value: provider,
label,
color: '#6b7280', // gray fallback
})
}
}
const integrationOptions = Array.from(providerMap.values()).sort((a, b) =>
a.label.localeCompare(b.label)
)
cachedTriggerOptions = [...coreTypes, ...integrationOptions]
return cachedTriggerOptions
}
/**
* Formats a provider name into a display-friendly label
* e.g., "microsoft_teams" -> "Microsoft Teams"
*/
function formatProviderName(provider: string): string {
return provider
.split(/[-_]/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
/**
* Internal: Initialize metadata map for O(1) lookups
* Converts array of options to Map for fast access
*/
function initializeTriggerMetadataMap(): Map<string, { label: string; color: string }> {
if (cachedTriggerMetadataMap) {
return cachedTriggerMetadataMap
}
const options = getTriggerOptions()
cachedTriggerMetadataMap = new Map(
options.map((opt) => [opt.value, { label: opt.label, color: opt.color }])
)
return cachedTriggerMetadataMap
}
/**
* Gets integration metadata (label and color) for a specific trigger type.
*/
export function getIntegrationMetadata(triggerType: string): { label: string; color: string } {
const metadataMap = initializeTriggerMetadataMap()
const found = metadataMap.get(triggerType)
if (found) {
return found
}
return {
label: formatProviderName(triggerType),
color: '#6b7280', // gray
}
}

View File

@@ -22,6 +22,12 @@ export interface FolderData {
name: string
}
export interface TriggerData {
value: string
label: string
color: string
}
export const FILTER_DEFINITIONS: FilterDefinition[] = [
{
key: 'level',
@@ -32,18 +38,8 @@ export const FILTER_DEFINITIONS: FilterDefinition[] = [
{ value: 'info', label: 'Info', description: 'Info logs only' },
],
},
{
key: 'trigger',
label: 'Trigger',
description: 'Filter by trigger type',
options: [
{ value: 'api', label: 'API', description: 'API-triggered executions' },
{ value: 'manual', label: 'Manual', description: 'Manually triggered executions' },
{ value: 'webhook', label: 'Webhook', description: 'Webhook-triggered executions' },
{ value: 'chat', label: 'Chat', description: 'Chat-triggered executions' },
{ value: 'schedule', label: 'Schedule', description: 'Scheduled executions' },
],
},
// Note: Trigger options are now dynamically populated from active logs
// Core types are included by default, integration triggers are added from actual log data
{
key: 'cost',
label: 'Cost',
@@ -86,18 +82,45 @@ export const FILTER_DEFINITIONS: FilterDefinition[] = [
},
]
// Core trigger types that are always available
const CORE_TRIGGERS: TriggerData[] = [
{ value: 'api', label: 'API', color: '#3b82f6' }, // blue-500
{ value: 'manual', label: 'Manual', color: '#6b7280' }, // gray-500
{ value: 'webhook', label: 'Webhook', color: '#f97316' }, // orange-500
{ value: 'chat', label: 'Chat', color: '#8b5cf6' }, // purple-500
{ value: 'schedule', label: 'Schedule', color: '#10b981' }, // green-500
]
export class SearchSuggestions {
private workflowsData: WorkflowData[]
private foldersData: FolderData[]
private triggersData: TriggerData[]
constructor(workflowsData: WorkflowData[] = [], foldersData: FolderData[] = []) {
constructor(
workflowsData: WorkflowData[] = [],
foldersData: FolderData[] = [],
triggersData: TriggerData[] = []
) {
this.workflowsData = workflowsData
this.foldersData = foldersData
this.triggersData = triggersData
}
updateData(workflowsData: WorkflowData[] = [], foldersData: FolderData[] = []) {
updateData(
workflowsData: WorkflowData[] = [],
foldersData: FolderData[] = [],
triggersData: TriggerData[] = []
) {
this.workflowsData = workflowsData
this.foldersData = foldersData
this.triggersData = triggersData
}
/**
* Get all triggers (core + integrations)
*/
private getAllTriggers(): TriggerData[] {
return [...CORE_TRIGGERS, ...this.triggersData]
}
/**
@@ -144,6 +167,15 @@ export class SearchSuggestions {
})
}
// Add trigger key (always available - core types + integrations)
suggestions.push({
id: 'filter-key-trigger',
value: 'trigger:',
label: 'Trigger',
description: 'Filter by trigger type',
category: 'filters',
})
// Add workflow and folder keys
if (this.workflowsData.length > 0) {
suggestions.push({
@@ -218,6 +250,30 @@ export class SearchSuggestions {
: null
}
// Trigger filter values (core + integrations)
if (key === 'trigger') {
const allTriggers = this.getAllTriggers()
const suggestions = allTriggers
.filter((t) => !partial || t.label.toLowerCase().includes(partial.toLowerCase()))
.slice(0, 15) // Show more since we have core + integrations
.map((t) => ({
id: `filter-value-trigger-${t.value}`,
value: `trigger:${t.value}`,
label: t.label,
description: `${t.label}-triggered executions`,
category: 'trigger' as const,
color: t.color,
}))
return suggestions.length > 0
? {
type: 'filter-values',
filterKey: 'trigger',
suggestions,
}
: null
}
// Workflow filter values
if (key === 'workflow') {
const suggestions = this.workflowsData
@@ -290,6 +346,16 @@ export class SearchSuggestions {
allSuggestions.push(...matchingFilterValues)
}
// Match triggers
const matchingTriggers = this.getMatchingTriggers(query)
if (matchingTriggers.length > 0) {
sections.push({
title: 'TRIGGERS',
suggestions: matchingTriggers,
})
allSuggestions.push(...matchingTriggers)
}
// Match workflows
const matchingWorkflows = this.getMatchingWorkflows(query)
if (matchingWorkflows.length > 0) {
@@ -313,6 +379,7 @@ export class SearchSuggestions {
// Add filter keys if no specific matches
if (
matchingFilterValues.length === 0 &&
matchingTriggers.length === 0 &&
matchingWorkflows.length === 0 &&
matchingFolders.length === 0
) {
@@ -364,6 +431,40 @@ export class SearchSuggestions {
return matches.slice(0, 5)
}
/**
* Match triggers by label (core + integrations)
*/
private getMatchingTriggers(query: string): Suggestion[] {
if (!query.trim()) return []
const lowerQuery = query.toLowerCase()
const allTriggers = this.getAllTriggers()
const matches = allTriggers
.filter((trigger) => trigger.label.toLowerCase().includes(lowerQuery))
.sort((a, b) => {
const aLabel = a.label.toLowerCase()
const bLabel = b.label.toLowerCase()
if (aLabel === lowerQuery) return -1
if (bLabel === lowerQuery) return 1
if (aLabel.startsWith(lowerQuery) && !bLabel.startsWith(lowerQuery)) return -1
if (bLabel.startsWith(lowerQuery) && !aLabel.startsWith(lowerQuery)) return 1
return aLabel.localeCompare(bLabel)
})
.slice(0, 8)
.map((trigger) => ({
id: `trigger-match-${trigger.value}`,
value: `trigger:${trigger.value}`,
label: trigger.label,
description: `${trigger.label}-triggered executions`,
category: 'trigger' as const,
color: trigger.color,
}))
return matches
}
/**
* Match workflows by name/description
*/

View File

@@ -52,7 +52,7 @@ export interface ExecutionEnvironment {
}
export interface ExecutionTrigger {
type: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
type: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | string
source: string
data?: Record<string, unknown>
timestamp: string

View File

@@ -164,7 +164,7 @@ export type TimeRange =
| 'Past 30 days'
| 'All time'
export type LogLevel = 'error' | 'info' | 'all'
export type TriggerType = 'chat' | 'api' | 'webhook' | 'manual' | 'schedule' | 'all'
export type TriggerType = 'chat' | 'api' | 'webhook' | 'manual' | 'schedule' | 'all' | string
export interface FilterState {
// Workspace context