mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
91
apps/sim/app/api/logs/triggers/route.ts
Normal file
91
apps/sim/app/api/logs/triggers/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)]'>
|
||||
—
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface Suggestion {
|
||||
value: string
|
||||
label: string
|
||||
description?: string
|
||||
color?: string
|
||||
category?:
|
||||
| 'filters'
|
||||
| 'level'
|
||||
|
||||
@@ -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(),
|
||||
|
||||
117
apps/sim/lib/logs/get-trigger-options.ts
Normal file
117
apps/sim/lib/logs/get-trigger-options.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user