mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
feat(logs-context-menu): consolidated logs utils and types, added logs record context menu (#2659)
This commit is contained in:
@@ -30,6 +30,18 @@ import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/component
|
|||||||
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
|
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## No Re-exports
|
||||||
|
|
||||||
|
Do not re-export from non-barrel files. Import directly from the source.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✓ Good - import from where it's declared
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
|
|
||||||
|
// ✗ Bad - re-exporting in utils.ts then importing from there
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/app/workspace/.../utils'
|
||||||
|
```
|
||||||
|
|
||||||
## Import Order
|
## Import Order
|
||||||
|
|
||||||
1. React/core libraries
|
1. React/core libraries
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import { useWorkflowStore } from '@/stores/workflows/store'
|
|||||||
import { useWorkflowStore } from '../../../stores/workflows/store'
|
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||||
```
|
```
|
||||||
|
|
||||||
Use barrel exports (`index.ts`) when a folder has 3+ exports.
|
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
|
||||||
|
|
||||||
### Import Order
|
### Import Order
|
||||||
1. React/core libraries
|
1. React/core libraries
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
|||||||
import { processInputFileFields } from '@/lib/execution/files'
|
import { processInputFileFields } from '@/lib/execution/files'
|
||||||
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
||||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
|
||||||
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||||
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
|
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
|
||||||
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||||
@@ -27,13 +26,14 @@ import { normalizeName } from '@/executor/constants'
|
|||||||
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { Serializer } from '@/serializer'
|
import { Serializer } from '@/serializer'
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
import type { SubflowType } from '@/stores/workflows/workflow/types'
|
import type { SubflowType } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
const logger = createLogger('WorkflowExecuteAPI')
|
const logger = createLogger('WorkflowExecuteAPI')
|
||||||
|
|
||||||
const ExecuteWorkflowSchema = z.object({
|
const ExecuteWorkflowSchema = z.object({
|
||||||
selectedOutputs: z.array(z.string()).optional().default([]),
|
selectedOutputs: z.array(z.string()).optional().default([]),
|
||||||
triggerType: z.enum(ALL_TRIGGER_TYPES).optional(),
|
triggerType: z.enum(CORE_TRIGGER_TYPES).optional(),
|
||||||
stream: z.boolean().optional(),
|
stream: z.boolean().optional(),
|
||||||
useDraftState: z.boolean().optional(),
|
useDraftState: z.boolean().optional(),
|
||||||
input: z.any().optional(),
|
input: z.any().optional(),
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { type NextRequest, NextResponse } from 'next/server'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
|
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceNotificationAPI')
|
const logger = createLogger('WorkspaceNotificationAPI')
|
||||||
|
|
||||||
const levelFilterSchema = z.array(z.enum(['info', 'error']))
|
const levelFilterSchema = z.array(z.enum(['info', 'error']))
|
||||||
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
|
const triggerFilterSchema = z.array(z.enum(CORE_TRIGGER_TYPES))
|
||||||
|
|
||||||
const alertRuleSchema = z.enum([
|
const alertRuleSchema = z.enum([
|
||||||
'consecutive_failures',
|
'consecutive_failures',
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
|
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceNotificationsAPI')
|
const logger = createLogger('WorkspaceNotificationsAPI')
|
||||||
|
|
||||||
const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
|
const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
|
||||||
const levelFilterSchema = z.array(z.enum(['info', 'error']))
|
const levelFilterSchema = z.array(z.enum(['info', 'error']))
|
||||||
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
|
const triggerFilterSchema = z.array(z.enum(CORE_TRIGGER_TYPES))
|
||||||
|
|
||||||
const alertRuleSchema = z.enum([
|
const alertRuleSchema = z.enum([
|
||||||
'consecutive_failures',
|
'consecutive_failures',
|
||||||
@@ -81,7 +81,7 @@ const createNotificationSchema = z
|
|||||||
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
|
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
|
||||||
allWorkflows: z.boolean().default(false),
|
allWorkflows: z.boolean().default(false),
|
||||||
levelFilter: levelFilterSchema.default(['info', 'error']),
|
levelFilter: levelFilterSchema.default(['info', 'error']),
|
||||||
triggerFilter: triggerFilterSchema.default([...ALL_TRIGGER_TYPES]),
|
triggerFilter: triggerFilterSchema.default([...CORE_TRIGGER_TYPES]),
|
||||||
includeFinalOutput: z.boolean().default(false),
|
includeFinalOutput: z.boolean().default(false),
|
||||||
includeTraceSpans: z.boolean().default(false),
|
includeTraceSpans: z.boolean().default(false),
|
||||||
includeRateLimits: z.boolean().default(false),
|
includeRateLimits: z.boolean().default(false),
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export { LogDetails } from './log-details'
|
|||||||
export { FileCards } from './log-details/components/file-download'
|
export { FileCards } from './log-details/components/file-download'
|
||||||
export { FrozenCanvas } from './log-details/components/frozen-canvas'
|
export { FrozenCanvas } from './log-details/components/frozen-canvas'
|
||||||
export { TraceSpans } from './log-details/components/trace-spans'
|
export { TraceSpans } from './log-details/components/trace-spans'
|
||||||
|
export { LogRowContextMenu } from './log-row-context-menu'
|
||||||
export { LogsList } from './logs-list'
|
export { LogsList } from './logs-list'
|
||||||
export { AutocompleteSearch, LogsToolbar, NotificationSettings } from './logs-toolbar'
|
export { AutocompleteSearch, LogsToolbar, NotificationSettings } from './logs-toolbar'
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { LogRowContextMenu } from './log-row-context-menu'
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { RefObject } from 'react'
|
||||||
|
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||||
|
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||||
|
|
||||||
|
interface LogRowContextMenuProps {
|
||||||
|
isOpen: boolean
|
||||||
|
position: { x: number; y: number }
|
||||||
|
menuRef: RefObject<HTMLDivElement | null>
|
||||||
|
onClose: () => void
|
||||||
|
log: WorkflowLog | null
|
||||||
|
onCopyExecutionId: () => void
|
||||||
|
onOpenWorkflow: () => void
|
||||||
|
onToggleWorkflowFilter: () => void
|
||||||
|
onClearAllFilters: () => void
|
||||||
|
isFilteredByThisWorkflow: boolean
|
||||||
|
hasActiveFilters: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context menu for log rows.
|
||||||
|
* Provides quick actions for copying data, navigation, and filtering.
|
||||||
|
*/
|
||||||
|
export function LogRowContextMenu({
|
||||||
|
isOpen,
|
||||||
|
position,
|
||||||
|
menuRef,
|
||||||
|
onClose,
|
||||||
|
log,
|
||||||
|
onCopyExecutionId,
|
||||||
|
onOpenWorkflow,
|
||||||
|
onToggleWorkflowFilter,
|
||||||
|
onClearAllFilters,
|
||||||
|
isFilteredByThisWorkflow,
|
||||||
|
hasActiveFilters,
|
||||||
|
}: LogRowContextMenuProps) {
|
||||||
|
const hasExecutionId = Boolean(log?.executionId)
|
||||||
|
const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
|
||||||
|
<PopoverAnchor
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${position.x}px`,
|
||||||
|
top: `${position.y}px`,
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||||
|
{/* Copy Execution ID */}
|
||||||
|
<PopoverItem
|
||||||
|
disabled={!hasExecutionId}
|
||||||
|
onClick={() => {
|
||||||
|
onCopyExecutionId()
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Execution ID
|
||||||
|
</PopoverItem>
|
||||||
|
|
||||||
|
{/* Open Workflow */}
|
||||||
|
<PopoverItem
|
||||||
|
disabled={!hasWorkflow}
|
||||||
|
onClick={() => {
|
||||||
|
onOpenWorkflow()
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open Workflow
|
||||||
|
</PopoverItem>
|
||||||
|
|
||||||
|
{/* Filter by Workflow - only show when not already filtered by this workflow */}
|
||||||
|
{!isFilteredByThisWorkflow && (
|
||||||
|
<PopoverItem
|
||||||
|
disabled={!hasWorkflow}
|
||||||
|
onClick={() => {
|
||||||
|
onToggleWorkflowFilter()
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Filter by Workflow
|
||||||
|
</PopoverItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Clear All Filters - show when any filters are active */}
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<PopoverItem
|
||||||
|
onClick={() => {
|
||||||
|
onClearAllFilters()
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</PopoverItem>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
|
LOG_COLUMNS,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
TriggerBadge,
|
TriggerBadge,
|
||||||
} from '@/app/workspace/[workspaceId]/logs/utils'
|
} from '@/app/workspace/[workspaceId]/logs/utils'
|
||||||
@@ -21,6 +22,7 @@ interface LogRowProps {
|
|||||||
log: WorkflowLog
|
log: WorkflowLog
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
onClick: (log: WorkflowLog) => void
|
onClick: (log: WorkflowLog) => void
|
||||||
|
onContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
|
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +31,21 @@ interface LogRowProps {
|
|||||||
* Uses shallow comparison for the log object.
|
* Uses shallow comparison for the log object.
|
||||||
*/
|
*/
|
||||||
const LogRow = memo(
|
const LogRow = memo(
|
||||||
function LogRow({ log, isSelected, onClick, selectedRowRef }: LogRowProps) {
|
function LogRow({ log, isSelected, onClick, onContextMenu, selectedRowRef }: LogRowProps) {
|
||||||
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
||||||
|
|
||||||
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (onContextMenu) {
|
||||||
|
e.preventDefault()
|
||||||
|
onContextMenu(e, log)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onContextMenu, log]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={isSelected ? selectedRowRef : null}
|
ref={isSelected ? selectedRowRef : null}
|
||||||
@@ -42,25 +54,28 @@ const LogRow = memo(
|
|||||||
isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'
|
isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<div className='flex flex-1 items-center'>
|
<div className='flex flex-1 items-center'>
|
||||||
{/* Date */}
|
<span
|
||||||
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-primary)]'>
|
className={`${LOG_COLUMNS.date.width} ${LOG_COLUMNS.date.minWidth} font-medium text-[12px] text-[var(--text-primary)]`}
|
||||||
|
>
|
||||||
{formattedDate.compactDate}
|
{formattedDate.compactDate}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Time */}
|
<span
|
||||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
className={`${LOG_COLUMNS.time.width} ${LOG_COLUMNS.time.minWidth} font-medium text-[12px] text-[var(--text-primary)]`}
|
||||||
|
>
|
||||||
{formattedDate.compactTime}
|
{formattedDate.compactTime}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Status */}
|
<div className={`${LOG_COLUMNS.status.width} ${LOG_COLUMNS.status.minWidth}`}>
|
||||||
<div className='w-[12%] min-w-[100px]'>
|
|
||||||
<StatusBadge status={getDisplayStatus(log.status)} />
|
<StatusBadge status={getDisplayStatus(log.status)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workflow */}
|
<div
|
||||||
<div className='flex w-[22%] min-w-[140px] items-center gap-[8px] pr-[8px]'>
|
className={`flex ${LOG_COLUMNS.workflow.width} ${LOG_COLUMNS.workflow.minWidth} items-center gap-[8px] pr-[8px]`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||||
style={{ backgroundColor: log.workflow?.color }}
|
style={{ backgroundColor: log.workflow?.color }}
|
||||||
@@ -70,13 +85,13 @@ const LogRow = memo(
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cost */}
|
<span
|
||||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
className={`${LOG_COLUMNS.cost.width} ${LOG_COLUMNS.cost.minWidth} font-medium text-[12px] text-[var(--text-primary)]`}
|
||||||
|
>
|
||||||
{typeof log.cost?.total === 'number' ? `$${log.cost.total.toFixed(4)}` : '—'}
|
{typeof log.cost?.total === 'number' ? `$${log.cost.total.toFixed(4)}` : '—'}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Trigger */}
|
<div className={`${LOG_COLUMNS.trigger.width} ${LOG_COLUMNS.trigger.minWidth}`}>
|
||||||
<div className='w-[14%] min-w-[110px]'>
|
|
||||||
{log.trigger ? (
|
{log.trigger ? (
|
||||||
<TriggerBadge trigger={log.trigger} />
|
<TriggerBadge trigger={log.trigger} />
|
||||||
) : (
|
) : (
|
||||||
@@ -84,8 +99,7 @@ const LogRow = memo(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Duration */}
|
<div className={`${LOG_COLUMNS.duration.width} ${LOG_COLUMNS.duration.minWidth}`}>
|
||||||
<div className='w-[20%] min-w-[100px]'>
|
|
||||||
<Badge variant='default' className='rounded-[6px] px-[9px] py-[2px] text-[12px]'>
|
<Badge variant='default' className='rounded-[6px] px-[9px] py-[2px] text-[12px]'>
|
||||||
{formatDuration(log.duration) || '—'}
|
{formatDuration(log.duration) || '—'}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -125,6 +139,7 @@ interface RowProps {
|
|||||||
logs: WorkflowLog[]
|
logs: WorkflowLog[]
|
||||||
selectedLogId: string | null
|
selectedLogId: string | null
|
||||||
onLogClick: (log: WorkflowLog) => void
|
onLogClick: (log: WorkflowLog) => void
|
||||||
|
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||||
isFetchingNextPage: boolean
|
isFetchingNextPage: boolean
|
||||||
loaderRef: React.RefObject<HTMLDivElement | null>
|
loaderRef: React.RefObject<HTMLDivElement | null>
|
||||||
@@ -140,11 +155,11 @@ function Row({
|
|||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
loaderRef,
|
loaderRef,
|
||||||
}: RowComponentProps<RowProps>) {
|
}: RowComponentProps<RowProps>) {
|
||||||
// Show loader for the last item if loading more
|
|
||||||
if (index >= logs.length) {
|
if (index >= logs.length) {
|
||||||
return (
|
return (
|
||||||
<div style={style} className='flex items-center justify-center'>
|
<div style={style} className='flex items-center justify-center'>
|
||||||
@@ -171,6 +186,7 @@ function Row({
|
|||||||
log={log}
|
log={log}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onClick={onLogClick}
|
onClick={onLogClick}
|
||||||
|
onContextMenu={onLogContextMenu}
|
||||||
selectedRowRef={isSelected ? selectedRowRef : null}
|
selectedRowRef={isSelected ? selectedRowRef : null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +197,7 @@ export interface LogsListProps {
|
|||||||
logs: WorkflowLog[]
|
logs: WorkflowLog[]
|
||||||
selectedLogId: string | null
|
selectedLogId: string | null
|
||||||
onLogClick: (log: WorkflowLog) => void
|
onLogClick: (log: WorkflowLog) => void
|
||||||
|
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||||
hasNextPage: boolean
|
hasNextPage: boolean
|
||||||
isFetchingNextPage: boolean
|
isFetchingNextPage: boolean
|
||||||
@@ -198,6 +215,7 @@ export function LogsList({
|
|||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
@@ -208,7 +226,6 @@ export function LogsList({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [listHeight, setListHeight] = useState(400)
|
const [listHeight, setListHeight] = useState(400)
|
||||||
|
|
||||||
// Measure container height for virtualization
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
if (!container) return
|
if (!container) return
|
||||||
@@ -226,7 +243,6 @@ export function LogsList({
|
|||||||
return () => ro.disconnect()
|
return () => ro.disconnect()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handle infinite scroll when nearing the end of the list
|
|
||||||
const handleRowsRendered = useCallback(
|
const handleRowsRendered = useCallback(
|
||||||
({ stopIndex }: { startIndex: number; stopIndex: number }) => {
|
({ stopIndex }: { startIndex: number; stopIndex: number }) => {
|
||||||
const threshold = logs.length - 10
|
const threshold = logs.length - 10
|
||||||
@@ -237,20 +253,27 @@ export function LogsList({
|
|||||||
[logs.length, hasNextPage, isFetchingNextPage, onLoadMore]
|
[logs.length, hasNextPage, isFetchingNextPage, onLoadMore]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate total item count including loader row
|
|
||||||
const itemCount = hasNextPage ? logs.length + 1 : logs.length
|
const itemCount = hasNextPage ? logs.length + 1 : logs.length
|
||||||
|
|
||||||
// Row props passed to each row component
|
|
||||||
const rowProps = useMemo<RowProps>(
|
const rowProps = useMemo<RowProps>(
|
||||||
() => ({
|
() => ({
|
||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
loaderRef,
|
loaderRef,
|
||||||
}),
|
}),
|
||||||
[logs, selectedLogId, onLogClick, selectedRowRef, isFetchingNextPage, loaderRef]
|
[
|
||||||
|
logs,
|
||||||
|
selectedLogId,
|
||||||
|
onLogClick,
|
||||||
|
onLogContextMenu,
|
||||||
|
selectedRowRef,
|
||||||
|
isFetchingNextPage,
|
||||||
|
loaderRef,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
import { SlackIcon } from '@/components/icons'
|
import { SlackIcon } from '@/components/icons'
|
||||||
import { Skeleton } from '@/components/ui'
|
import { Skeleton } from '@/components/ui'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { ALL_TRIGGER_TYPES, type TriggerType } from '@/lib/logs/types'
|
|
||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import {
|
import {
|
||||||
type NotificationSubscription,
|
type NotificationSubscription,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
} from '@/hooks/queries/notifications'
|
} from '@/hooks/queries/notifications'
|
||||||
import { useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
import { useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
||||||
import { useSlackAccounts } from '@/hooks/use-slack-accounts'
|
import { useSlackAccounts } from '@/hooks/use-slack-accounts'
|
||||||
|
import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types'
|
||||||
import { SlackChannelSelector } from './components/slack-channel-selector'
|
import { SlackChannelSelector } from './components/slack-channel-selector'
|
||||||
import { WorkflowSelector } from './components/workflow-selector'
|
import { WorkflowSelector } from './components/workflow-selector'
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export function NotificationSettings({
|
|||||||
workflowIds: [] as string[],
|
workflowIds: [] as string[],
|
||||||
allWorkflows: true,
|
allWorkflows: true,
|
||||||
levelFilter: ['info', 'error'] as LogLevel[],
|
levelFilter: ['info', 'error'] as LogLevel[],
|
||||||
triggerFilter: [...ALL_TRIGGER_TYPES] as TriggerType[],
|
triggerFilter: [...CORE_TRIGGER_TYPES] as CoreTriggerType[],
|
||||||
includeFinalOutput: false,
|
includeFinalOutput: false,
|
||||||
includeTraceSpans: false,
|
includeTraceSpans: false,
|
||||||
includeRateLimits: false,
|
includeRateLimits: false,
|
||||||
@@ -203,7 +203,7 @@ export function NotificationSettings({
|
|||||||
workflowIds: [],
|
workflowIds: [],
|
||||||
allWorkflows: true,
|
allWorkflows: true,
|
||||||
levelFilter: ['info', 'error'],
|
levelFilter: ['info', 'error'],
|
||||||
triggerFilter: [...ALL_TRIGGER_TYPES],
|
triggerFilter: [...CORE_TRIGGER_TYPES],
|
||||||
includeFinalOutput: false,
|
includeFinalOutput: false,
|
||||||
includeTraceSpans: false,
|
includeTraceSpans: false,
|
||||||
includeRateLimits: false,
|
includeRateLimits: false,
|
||||||
@@ -516,7 +516,7 @@ export function NotificationSettings({
|
|||||||
workflowIds: subscription.workflowIds || [],
|
workflowIds: subscription.workflowIds || [],
|
||||||
allWorkflows: subscription.allWorkflows,
|
allWorkflows: subscription.allWorkflows,
|
||||||
levelFilter: subscription.levelFilter as LogLevel[],
|
levelFilter: subscription.levelFilter as LogLevel[],
|
||||||
triggerFilter: subscription.triggerFilter as TriggerType[],
|
triggerFilter: subscription.triggerFilter as CoreTriggerType[],
|
||||||
includeFinalOutput: subscription.includeFinalOutput,
|
includeFinalOutput: subscription.includeFinalOutput,
|
||||||
includeTraceSpans: subscription.includeTraceSpans,
|
includeTraceSpans: subscription.includeTraceSpans,
|
||||||
includeRateLimits: subscription.includeRateLimits,
|
includeRateLimits: subscription.includeRateLimits,
|
||||||
@@ -849,14 +849,14 @@ export function NotificationSettings({
|
|||||||
<div className='flex flex-col gap-[8px]'>
|
<div className='flex flex-col gap-[8px]'>
|
||||||
<Label className='text-[var(--text-secondary)]'>Trigger Type Filters</Label>
|
<Label className='text-[var(--text-secondary)]'>Trigger Type Filters</Label>
|
||||||
<Combobox
|
<Combobox
|
||||||
options={ALL_TRIGGER_TYPES.map((trigger) => ({
|
options={CORE_TRIGGER_TYPES.map((trigger) => ({
|
||||||
label: trigger.charAt(0).toUpperCase() + trigger.slice(1),
|
label: trigger.charAt(0).toUpperCase() + trigger.slice(1),
|
||||||
value: trigger,
|
value: trigger,
|
||||||
}))}
|
}))}
|
||||||
multiSelect
|
multiSelect
|
||||||
multiSelectValues={formData.triggerFilter}
|
multiSelectValues={formData.triggerFilter}
|
||||||
onMultiSelectChange={(values) => {
|
onMultiSelectChange={(values) => {
|
||||||
setFormData({ ...formData, triggerFilter: values as TriggerType[] })
|
setFormData({ ...formData, triggerFilter: values as CoreTriggerType[] })
|
||||||
setFormErrors({ ...formErrors, triggerFilter: '' })
|
setFormErrors({ ...formErrors, triggerFilter: '' })
|
||||||
}}
|
}}
|
||||||
placeholder='Select trigger types...'
|
placeholder='Select trigger types...'
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ import {
|
|||||||
} from '@/components/emcn'
|
} from '@/components/emcn'
|
||||||
import { DatePicker } from '@/components/emcn/components/date-picker/date-picker'
|
import { DatePicker } from '@/components/emcn/components/date-picker/date-picker'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
|
import { hasActiveFilters } from '@/lib/logs/filters'
|
||||||
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import { useFolderStore } from '@/stores/folders/store'
|
import { useFolderStore } from '@/stores/folders/store'
|
||||||
import { useFilterStore } from '@/stores/logs/filters/store'
|
import { useFilterStore } from '@/stores/logs/filters/store'
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { AutocompleteSearch } from './components/search'
|
import { AutocompleteSearch } from './components/search'
|
||||||
|
|
||||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
|
||||||
|
|
||||||
const TIME_RANGE_OPTIONS: ComboboxOption[] = [
|
const TIME_RANGE_OPTIONS: ComboboxOption[] = [
|
||||||
{ value: 'All time', label: 'All time' },
|
{ value: 'All time', label: 'All time' },
|
||||||
{ value: 'Past 30 minutes', label: 'Past 30 minutes' },
|
{ value: 'Past 30 minutes', label: 'Past 30 minutes' },
|
||||||
@@ -182,6 +182,7 @@ export function LogsToolbar({
|
|||||||
endDate,
|
endDate,
|
||||||
setDateRange,
|
setDateRange,
|
||||||
clearDateRange,
|
clearDateRange,
|
||||||
|
resetFilters,
|
||||||
} = useFilterStore()
|
} = useFilterStore()
|
||||||
|
|
||||||
const [datePickerOpen, setDatePickerOpen] = useState(false)
|
const [datePickerOpen, setDatePickerOpen] = useState(false)
|
||||||
@@ -346,23 +347,23 @@ export function LogsToolbar({
|
|||||||
setDatePickerOpen(false)
|
setDatePickerOpen(false)
|
||||||
}, [timeRange, startDate, previousTimeRange, setTimeRange])
|
}, [timeRange, startDate, previousTimeRange, setTimeRange])
|
||||||
|
|
||||||
const hasActiveFilters = useMemo(() => {
|
const filtersActive = useMemo(
|
||||||
return (
|
() =>
|
||||||
level !== 'all' ||
|
hasActiveFilters({
|
||||||
workflowIds.length > 0 ||
|
timeRange,
|
||||||
folderIds.length > 0 ||
|
level,
|
||||||
triggers.length > 0 ||
|
workflowIds,
|
||||||
timeRange !== 'All time'
|
folderIds,
|
||||||
)
|
triggers,
|
||||||
}, [level, workflowIds, folderIds, triggers, timeRange])
|
searchQuery,
|
||||||
|
}),
|
||||||
|
[timeRange, level, workflowIds, folderIds, triggers, searchQuery]
|
||||||
|
)
|
||||||
|
|
||||||
const handleClearFilters = useCallback(() => {
|
const handleClearFilters = useCallback(() => {
|
||||||
setLevel('all')
|
resetFilters()
|
||||||
setWorkflowIds([])
|
onSearchQueryChange('')
|
||||||
setFolderIds([])
|
}, [resetFilters, onSearchQueryChange])
|
||||||
setTriggers([])
|
|
||||||
clearDateRange()
|
|
||||||
}, [setLevel, setWorkflowIds, setFolderIds, setTriggers, clearDateRange])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-[19px]'>
|
<div className='flex flex-col gap-[19px]'>
|
||||||
@@ -462,7 +463,7 @@ export function LogsToolbar({
|
|||||||
</div>
|
</div>
|
||||||
<div className='ml-auto flex items-center gap-[8px]'>
|
<div className='ml-auto flex items-center gap-[8px]'>
|
||||||
{/* Clear Filters Button */}
|
{/* Clear Filters Button */}
|
||||||
{hasActiveFilters && (
|
{filtersActive && (
|
||||||
<Button
|
<Button
|
||||||
variant='active'
|
variant='active'
|
||||||
onClick={handleClearFilters}
|
onClick={handleClearFilters}
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters'
|
import {
|
||||||
|
getEndDateFromTimeRange,
|
||||||
|
getStartDateFromTimeRange,
|
||||||
|
hasActiveFilters,
|
||||||
|
} from '@/lib/logs/filters'
|
||||||
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
||||||
import { useFolders } from '@/hooks/queries/folders'
|
import { useFolders } from '@/hooks/queries/folders'
|
||||||
import { useDashboardLogs, useLogDetail, useLogsList } from '@/hooks/queries/logs'
|
import { useDashboardLogs, useLogDetail, useLogsList } from '@/hooks/queries/logs'
|
||||||
@@ -12,7 +16,15 @@ import { useDebounce } from '@/hooks/use-debounce'
|
|||||||
import { useFilterStore } from '@/stores/logs/filters/store'
|
import { useFilterStore } from '@/stores/logs/filters/store'
|
||||||
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||||
import { useUserPermissionsContext } from '../providers/workspace-permissions-provider'
|
import { useUserPermissionsContext } from '../providers/workspace-permissions-provider'
|
||||||
import { Dashboard, LogDetails, LogsList, LogsToolbar, NotificationSettings } from './components'
|
import {
|
||||||
|
Dashboard,
|
||||||
|
LogDetails,
|
||||||
|
LogRowContextMenu,
|
||||||
|
LogsList,
|
||||||
|
LogsToolbar,
|
||||||
|
NotificationSettings,
|
||||||
|
} from './components'
|
||||||
|
import { LOG_COLUMN_ORDER, LOG_COLUMNS } from './utils'
|
||||||
|
|
||||||
const LOGS_PER_PAGE = 50 as const
|
const LOGS_PER_PAGE = 50 as const
|
||||||
const REFRESH_SPINNER_DURATION_MS = 1000 as const
|
const REFRESH_SPINNER_DURATION_MS = 1000 as const
|
||||||
@@ -35,10 +47,12 @@ export default function Logs() {
|
|||||||
level,
|
level,
|
||||||
workflowIds,
|
workflowIds,
|
||||||
folderIds,
|
folderIds,
|
||||||
|
setWorkflowIds,
|
||||||
setSearchQuery: setStoreSearchQuery,
|
setSearchQuery: setStoreSearchQuery,
|
||||||
triggers,
|
triggers,
|
||||||
viewMode,
|
viewMode,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
|
resetFilters,
|
||||||
} = useFilterStore()
|
} = useFilterStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -71,6 +85,11 @@ export default function Logs() {
|
|||||||
const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false)
|
const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false)
|
||||||
const userPermissions = useUserPermissionsContext()
|
const userPermissions = useUserPermissionsContext()
|
||||||
|
|
||||||
|
const [contextMenuOpen, setContextMenuOpen] = useState(false)
|
||||||
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
||||||
|
const [contextMenuLog, setContextMenuLog] = useState<WorkflowLog | null>(null)
|
||||||
|
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const logFilters = useMemo(
|
const logFilters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
timeRange,
|
timeRange,
|
||||||
@@ -216,6 +235,56 @@ export default function Logs() {
|
|||||||
prevSelectedLogRef.current = null
|
prevSelectedLogRef.current = null
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleLogContextMenu = useCallback((e: React.MouseEvent, log: WorkflowLog) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setContextMenuPosition({ x: e.clientX, y: e.clientY })
|
||||||
|
setContextMenuLog(log)
|
||||||
|
setContextMenuOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCopyExecutionId = useCallback(() => {
|
||||||
|
if (contextMenuLog?.executionId) {
|
||||||
|
navigator.clipboard.writeText(contextMenuLog.executionId)
|
||||||
|
}
|
||||||
|
}, [contextMenuLog])
|
||||||
|
|
||||||
|
const handleOpenWorkflow = useCallback(() => {
|
||||||
|
const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
|
||||||
|
if (wfId) {
|
||||||
|
window.open(`/workspace/${workspaceId}/w/${wfId}`, '_blank')
|
||||||
|
}
|
||||||
|
}, [contextMenuLog, workspaceId])
|
||||||
|
|
||||||
|
const handleToggleWorkflowFilter = useCallback(() => {
|
||||||
|
const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
|
||||||
|
if (!wfId) return
|
||||||
|
|
||||||
|
if (workflowIds.length === 1 && workflowIds[0] === wfId) {
|
||||||
|
setWorkflowIds([])
|
||||||
|
} else {
|
||||||
|
setWorkflowIds([wfId])
|
||||||
|
}
|
||||||
|
}, [contextMenuLog, workflowIds, setWorkflowIds])
|
||||||
|
|
||||||
|
const handleClearAllFilters = useCallback(() => {
|
||||||
|
resetFilters()
|
||||||
|
setSearchQuery('')
|
||||||
|
}, [resetFilters, setSearchQuery])
|
||||||
|
|
||||||
|
const contextMenuWorkflowId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
|
||||||
|
const isFilteredByThisWorkflow = Boolean(
|
||||||
|
contextMenuWorkflowId && workflowIds.length === 1 && workflowIds[0] === contextMenuWorkflowId
|
||||||
|
)
|
||||||
|
|
||||||
|
const filtersActive = hasActiveFilters({
|
||||||
|
timeRange,
|
||||||
|
level,
|
||||||
|
workflowIds,
|
||||||
|
folderIds,
|
||||||
|
triggers,
|
||||||
|
searchQuery: debouncedSearchQuery,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedRowRef.current) {
|
if (selectedRowRef.current) {
|
||||||
selectedRowRef.current.scrollIntoView({
|
selectedRowRef.current.scrollIntoView({
|
||||||
@@ -400,27 +469,17 @@ export default function Logs() {
|
|||||||
{/* Table header */}
|
{/* Table header */}
|
||||||
<div className='flex-shrink-0 rounded-t-[6px] bg-[var(--surface-3)] px-[24px] py-[10px] dark:bg-[var(--surface-3)]'>
|
<div className='flex-shrink-0 rounded-t-[6px] bg-[var(--surface-3)] px-[24px] py-[10px] dark:bg-[var(--surface-3)]'>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
{LOG_COLUMN_ORDER.map((key) => {
|
||||||
Date
|
const col = LOG_COLUMNS[key]
|
||||||
</span>
|
return (
|
||||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
<span
|
||||||
Time
|
key={key}
|
||||||
</span>
|
className={`${col.width} ${col.minWidth} font-medium text-[12px] text-[var(--text-tertiary)]`}
|
||||||
<span className='w-[12%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
>
|
||||||
Status
|
{col.label}
|
||||||
</span>
|
</span>
|
||||||
<span className='w-[22%] min-w-[140px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
)
|
||||||
Workflow
|
})}
|
||||||
</span>
|
|
||||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
|
||||||
Cost
|
|
||||||
</span>
|
|
||||||
<span className='w-[14%] min-w-[110px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
|
||||||
Trigger
|
|
||||||
</span>
|
|
||||||
<span className='w-[20%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
|
||||||
Duration
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -452,6 +511,7 @@ export default function Logs() {
|
|||||||
logs={logs}
|
logs={logs}
|
||||||
selectedLogId={selectedLog?.id ?? null}
|
selectedLogId={selectedLog?.id ?? null}
|
||||||
onLogClick={handleLogClick}
|
onLogClick={handleLogClick}
|
||||||
|
onLogContextMenu={handleLogContextMenu}
|
||||||
selectedRowRef={selectedRowRef}
|
selectedRowRef={selectedRowRef}
|
||||||
hasNextPage={logsQuery.hasNextPage ?? false}
|
hasNextPage={logsQuery.hasNextPage ?? false}
|
||||||
isFetchingNextPage={logsQuery.isFetchingNextPage}
|
isFetchingNextPage={logsQuery.isFetchingNextPage}
|
||||||
@@ -481,6 +541,20 @@ export default function Logs() {
|
|||||||
open={isNotificationSettingsOpen}
|
open={isNotificationSettingsOpen}
|
||||||
onOpenChange={setIsNotificationSettingsOpen}
|
onOpenChange={setIsNotificationSettingsOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LogRowContextMenu
|
||||||
|
isOpen={contextMenuOpen}
|
||||||
|
position={contextMenuPosition}
|
||||||
|
menuRef={contextMenuRef}
|
||||||
|
onClose={() => setContextMenuOpen(false)}
|
||||||
|
log={contextMenuLog}
|
||||||
|
onCopyExecutionId={handleCopyExecutionId}
|
||||||
|
onOpenWorkflow={handleOpenWorkflow}
|
||||||
|
onToggleWorkflowFilter={handleToggleWorkflowFilter}
|
||||||
|
onClearAllFilters={handleClearAllFilters}
|
||||||
|
isFilteredByThisWorkflow={isFilteredByThisWorkflow}
|
||||||
|
hasActiveFilters={filtersActive}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,32 @@ import { format } from 'date-fns'
|
|||||||
import { Badge } from '@/components/emcn'
|
import { Badge } from '@/components/emcn'
|
||||||
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
|
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||||
|
|
||||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
/** Column configuration for logs table - shared between header and rows */
|
||||||
|
export const LOG_COLUMNS = {
|
||||||
|
date: { width: 'w-[8%]', minWidth: 'min-w-[70px]', label: 'Date' },
|
||||||
|
time: { width: 'w-[12%]', minWidth: 'min-w-[90px]', label: 'Time' },
|
||||||
|
status: { width: 'w-[12%]', minWidth: 'min-w-[100px]', label: 'Status' },
|
||||||
|
workflow: { width: 'w-[22%]', minWidth: 'min-w-[140px]', label: 'Workflow' },
|
||||||
|
cost: { width: 'w-[12%]', minWidth: 'min-w-[90px]', label: 'Cost' },
|
||||||
|
trigger: { width: 'w-[14%]', minWidth: 'min-w-[110px]', label: 'Trigger' },
|
||||||
|
duration: { width: 'w-[20%]', minWidth: 'min-w-[100px]', label: 'Duration' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** Type-safe column key derived from LOG_COLUMNS */
|
||||||
|
export type LogColumnKey = keyof typeof LOG_COLUMNS
|
||||||
|
|
||||||
|
/** Ordered list of column keys for rendering table headers */
|
||||||
|
export const LOG_COLUMN_ORDER: readonly LogColumnKey[] = [
|
||||||
|
'date',
|
||||||
|
'time',
|
||||||
|
'status',
|
||||||
|
'workflow',
|
||||||
|
'cost',
|
||||||
|
'trigger',
|
||||||
|
'duration',
|
||||||
|
] as const
|
||||||
|
|
||||||
/** Possible execution status values for workflow logs */
|
/** Possible execution status values for workflow logs */
|
||||||
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
|
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
|
||||||
|
|||||||
@@ -24,11 +24,9 @@ export function useFloatBoundarySync({
|
|||||||
const positionRef = useRef(position)
|
const positionRef = useRef(position)
|
||||||
const previousDimensionsRef = useRef({ sidebarWidth: 0, panelWidth: 0, terminalHeight: 0 })
|
const previousDimensionsRef = useRef({ sidebarWidth: 0, panelWidth: 0, terminalHeight: 0 })
|
||||||
|
|
||||||
// Keep position ref up to date
|
|
||||||
positionRef.current = position
|
positionRef.current = position
|
||||||
|
|
||||||
const checkAndUpdatePosition = useCallback(() => {
|
const checkAndUpdatePosition = useCallback(() => {
|
||||||
// Get current layout dimensions
|
|
||||||
const sidebarWidth = Number.parseInt(
|
const sidebarWidth = Number.parseInt(
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||||
)
|
)
|
||||||
@@ -39,20 +37,17 @@ export function useFloatBoundarySync({
|
|||||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if dimensions actually changed
|
|
||||||
const prev = previousDimensionsRef.current
|
const prev = previousDimensionsRef.current
|
||||||
if (
|
if (
|
||||||
prev.sidebarWidth === sidebarWidth &&
|
prev.sidebarWidth === sidebarWidth &&
|
||||||
prev.panelWidth === panelWidth &&
|
prev.panelWidth === panelWidth &&
|
||||||
prev.terminalHeight === terminalHeight
|
prev.terminalHeight === terminalHeight
|
||||||
) {
|
) {
|
||||||
return // No change, skip update
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update previous dimensions
|
|
||||||
previousDimensionsRef.current = { sidebarWidth, panelWidth, terminalHeight }
|
previousDimensionsRef.current = { sidebarWidth, panelWidth, terminalHeight }
|
||||||
|
|
||||||
// Calculate bounds
|
|
||||||
const minX = sidebarWidth
|
const minX = sidebarWidth
|
||||||
const maxX = window.innerWidth - panelWidth - width
|
const maxX = window.innerWidth - panelWidth - width
|
||||||
const minY = 0
|
const minY = 0
|
||||||
@@ -60,9 +55,7 @@ export function useFloatBoundarySync({
|
|||||||
|
|
||||||
const currentPos = positionRef.current
|
const currentPos = positionRef.current
|
||||||
|
|
||||||
// Check if current position is out of bounds
|
|
||||||
if (currentPos.x < minX || currentPos.x > maxX || currentPos.y < minY || currentPos.y > maxY) {
|
if (currentPos.x < minX || currentPos.x > maxX || currentPos.y < minY || currentPos.y > maxY) {
|
||||||
// Constrain to new bounds
|
|
||||||
const newPosition = {
|
const newPosition = {
|
||||||
x: Math.max(minX, Math.min(maxX, currentPos.x)),
|
x: Math.max(minX, Math.min(maxX, currentPos.x)),
|
||||||
y: Math.max(minY, Math.min(maxY, currentPos.y)),
|
y: Math.max(minY, Math.min(maxY, currentPos.y)),
|
||||||
@@ -75,30 +68,24 @@ export function useFloatBoundarySync({
|
|||||||
if (!isOpen) return
|
if (!isOpen) return
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
// Cancel any pending animation frame
|
|
||||||
if (rafIdRef.current !== null) {
|
if (rafIdRef.current !== null) {
|
||||||
cancelAnimationFrame(rafIdRef.current)
|
cancelAnimationFrame(rafIdRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule update on next animation frame for smooth 60fps updates
|
|
||||||
rafIdRef.current = requestAnimationFrame(() => {
|
rafIdRef.current = requestAnimationFrame(() => {
|
||||||
checkAndUpdatePosition()
|
checkAndUpdatePosition()
|
||||||
rafIdRef.current = null
|
rafIdRef.current = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for window resize
|
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
// Create MutationObserver to watch for CSS variable changes
|
|
||||||
// This fires immediately when sidebar/panel/terminal resize
|
|
||||||
const observer = new MutationObserver(handleResize)
|
const observer = new MutationObserver(handleResize)
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['style'],
|
attributeFilter: ['style'],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initial check
|
|
||||||
checkAndUpdatePosition()
|
checkAndUpdatePosition()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
|||||||
*/
|
*/
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
// Only left click
|
|
||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -32,7 +31,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
|||||||
dragStartRef.current = { x: e.clientX, y: e.clientY }
|
dragStartRef.current = { x: e.clientX, y: e.clientY }
|
||||||
initialPositionRef.current = { ...position }
|
initialPositionRef.current = { ...position }
|
||||||
|
|
||||||
// Add dragging cursor to body
|
|
||||||
document.body.style.cursor = 'grabbing'
|
document.body.style.cursor = 'grabbing'
|
||||||
document.body.style.userSelect = 'none'
|
document.body.style.userSelect = 'none'
|
||||||
},
|
},
|
||||||
@@ -54,7 +52,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
|||||||
y: initialPositionRef.current.y + deltaY,
|
y: initialPositionRef.current.y + deltaY,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constrain to bounds
|
|
||||||
const constrainedPosition = constrainChatPosition(newPosition, width, height)
|
const constrainedPosition = constrainChatPosition(newPosition, width, height)
|
||||||
onPositionChange(constrainedPosition)
|
onPositionChange(constrainedPosition)
|
||||||
},
|
},
|
||||||
@@ -69,7 +66,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
|||||||
|
|
||||||
isDraggingRef.current = false
|
isDraggingRef.current = false
|
||||||
|
|
||||||
// Remove dragging cursor
|
|
||||||
document.body.style.cursor = ''
|
document.body.style.cursor = ''
|
||||||
document.body.style.userSelect = ''
|
document.body.style.userSelect = ''
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -84,13 +84,11 @@ export function useFloatResize({
|
|||||||
const isNearLeft = x <= EDGE_THRESHOLD
|
const isNearLeft = x <= EDGE_THRESHOLD
|
||||||
const isNearRight = x >= rect.width - EDGE_THRESHOLD
|
const isNearRight = x >= rect.width - EDGE_THRESHOLD
|
||||||
|
|
||||||
// Check corners first (they take priority over edges)
|
|
||||||
if (isNearTop && isNearLeft) return 'top-left'
|
if (isNearTop && isNearLeft) return 'top-left'
|
||||||
if (isNearTop && isNearRight) return 'top-right'
|
if (isNearTop && isNearRight) return 'top-right'
|
||||||
if (isNearBottom && isNearLeft) return 'bottom-left'
|
if (isNearBottom && isNearLeft) return 'bottom-left'
|
||||||
if (isNearBottom && isNearRight) return 'bottom-right'
|
if (isNearBottom && isNearRight) return 'bottom-right'
|
||||||
|
|
||||||
// Check edges
|
|
||||||
if (isNearTop) return 'top'
|
if (isNearTop) return 'top'
|
||||||
if (isNearBottom) return 'bottom'
|
if (isNearBottom) return 'bottom'
|
||||||
if (isNearLeft) return 'left'
|
if (isNearLeft) return 'left'
|
||||||
@@ -155,7 +153,6 @@ export function useFloatResize({
|
|||||||
*/
|
*/
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
// Only left click
|
|
||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
|
|
||||||
const chatElement = e.currentTarget as HTMLElement
|
const chatElement = e.currentTarget as HTMLElement
|
||||||
@@ -176,7 +173,6 @@ export function useFloatResize({
|
|||||||
height,
|
height,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cursor on body
|
|
||||||
document.body.style.cursor = getCursorForDirection(direction)
|
document.body.style.cursor = getCursorForDirection(direction)
|
||||||
document.body.style.userSelect = 'none'
|
document.body.style.userSelect = 'none'
|
||||||
},
|
},
|
||||||
@@ -195,7 +191,6 @@ export function useFloatResize({
|
|||||||
const initial = initialStateRef.current
|
const initial = initialStateRef.current
|
||||||
const direction = activeDirectionRef.current
|
const direction = activeDirectionRef.current
|
||||||
|
|
||||||
// Get layout bounds
|
|
||||||
const sidebarWidth = Number.parseInt(
|
const sidebarWidth = Number.parseInt(
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||||
)
|
)
|
||||||
@@ -206,18 +201,13 @@ export function useFloatResize({
|
|||||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clamp vertical drag when resizing from the top so the chat does not grow downward
|
|
||||||
// after its top edge hits the top of the viewport.
|
|
||||||
if (direction === 'top' || direction === 'top-left' || direction === 'top-right') {
|
if (direction === 'top' || direction === 'top-left' || direction === 'top-right') {
|
||||||
// newY = initial.y + deltaY should never be less than 0
|
|
||||||
const maxUpwardDelta = initial.y
|
const maxUpwardDelta = initial.y
|
||||||
if (deltaY < -maxUpwardDelta) {
|
if (deltaY < -maxUpwardDelta) {
|
||||||
deltaY = -maxUpwardDelta
|
deltaY = -maxUpwardDelta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp vertical drag when resizing from the bottom so the chat does not grow upward
|
|
||||||
// after its bottom edge hits the top of the terminal.
|
|
||||||
if (direction === 'bottom' || direction === 'bottom-left' || direction === 'bottom-right') {
|
if (direction === 'bottom' || direction === 'bottom-left' || direction === 'bottom-right') {
|
||||||
const maxBottom = window.innerHeight - terminalHeight
|
const maxBottom = window.innerHeight - terminalHeight
|
||||||
const initialBottom = initial.y + initial.height
|
const initialBottom = initial.y + initial.height
|
||||||
@@ -228,8 +218,6 @@ export function useFloatResize({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp horizontal drag when resizing from the left so the chat does not grow to the right
|
|
||||||
// after its left edge hits the sidebar.
|
|
||||||
if (direction === 'left' || direction === 'top-left' || direction === 'bottom-left') {
|
if (direction === 'left' || direction === 'top-left' || direction === 'bottom-left') {
|
||||||
const minLeft = sidebarWidth
|
const minLeft = sidebarWidth
|
||||||
const minDeltaX = minLeft - initial.x
|
const minDeltaX = minLeft - initial.x
|
||||||
@@ -239,8 +227,6 @@ export function useFloatResize({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp horizontal drag when resizing from the right so the chat does not grow to the left
|
|
||||||
// after its right edge hits the panel.
|
|
||||||
if (direction === 'right' || direction === 'top-right' || direction === 'bottom-right') {
|
if (direction === 'right' || direction === 'top-right' || direction === 'bottom-right') {
|
||||||
const maxRight = window.innerWidth - panelWidth
|
const maxRight = window.innerWidth - panelWidth
|
||||||
const initialRight = initial.x + initial.width
|
const initialRight = initial.x + initial.width
|
||||||
@@ -256,9 +242,7 @@ export function useFloatResize({
|
|||||||
let newWidth = initial.width
|
let newWidth = initial.width
|
||||||
let newHeight = initial.height
|
let newHeight = initial.height
|
||||||
|
|
||||||
// Calculate new dimensions based on resize direction
|
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
// Corners
|
|
||||||
case 'top-left':
|
case 'top-left':
|
||||||
newWidth = initial.width - deltaX
|
newWidth = initial.width - deltaX
|
||||||
newHeight = initial.height - deltaY
|
newHeight = initial.height - deltaY
|
||||||
@@ -280,7 +264,6 @@ export function useFloatResize({
|
|||||||
newHeight = initial.height + deltaY
|
newHeight = initial.height + deltaY
|
||||||
break
|
break
|
||||||
|
|
||||||
// Edges
|
|
||||||
case 'top':
|
case 'top':
|
||||||
newHeight = initial.height - deltaY
|
newHeight = initial.height - deltaY
|
||||||
newY = initial.y + deltaY
|
newY = initial.y + deltaY
|
||||||
@@ -297,8 +280,6 @@ export function useFloatResize({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constrain dimensions to min/max. If explicit constraints are not provided,
|
|
||||||
// fall back to the chat defaults for backward compatibility.
|
|
||||||
const effectiveMinWidth = typeof minWidth === 'number' ? minWidth : MIN_CHAT_WIDTH
|
const effectiveMinWidth = typeof minWidth === 'number' ? minWidth : MIN_CHAT_WIDTH
|
||||||
const effectiveMaxWidth = typeof maxWidth === 'number' ? maxWidth : MAX_CHAT_WIDTH
|
const effectiveMaxWidth = typeof maxWidth === 'number' ? maxWidth : MAX_CHAT_WIDTH
|
||||||
const effectiveMinHeight = typeof minHeight === 'number' ? minHeight : MIN_CHAT_HEIGHT
|
const effectiveMinHeight = typeof minHeight === 'number' ? minHeight : MIN_CHAT_HEIGHT
|
||||||
@@ -310,7 +291,6 @@ export function useFloatResize({
|
|||||||
Math.min(effectiveMaxHeight, newHeight)
|
Math.min(effectiveMaxHeight, newHeight)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Adjust position if dimensions were constrained on left/top edges
|
|
||||||
if (direction === 'top-left' || direction === 'bottom-left' || direction === 'left') {
|
if (direction === 'top-left' || direction === 'bottom-left' || direction === 'left') {
|
||||||
if (constrainedWidth !== newWidth) {
|
if (constrainedWidth !== newWidth) {
|
||||||
newX = initial.x + initial.width - constrainedWidth
|
newX = initial.x + initial.width - constrainedWidth
|
||||||
@@ -322,7 +302,6 @@ export function useFloatResize({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constrain position to bounds
|
|
||||||
const minX = sidebarWidth
|
const minX = sidebarWidth
|
||||||
const maxX = window.innerWidth - panelWidth - constrainedWidth
|
const maxX = window.innerWidth - panelWidth - constrainedWidth
|
||||||
const minY = 0
|
const minY = 0
|
||||||
@@ -331,7 +310,6 @@ export function useFloatResize({
|
|||||||
const finalX = Math.max(minX, Math.min(maxX, newX))
|
const finalX = Math.max(minX, Math.min(maxX, newX))
|
||||||
const finalY = Math.max(minY, Math.min(maxY, newY))
|
const finalY = Math.max(minY, Math.min(maxY, newY))
|
||||||
|
|
||||||
// Update state
|
|
||||||
onDimensionsChange({
|
onDimensionsChange({
|
||||||
width: constrainedWidth,
|
width: constrainedWidth,
|
||||||
height: constrainedHeight,
|
height: constrainedHeight,
|
||||||
@@ -353,7 +331,6 @@ export function useFloatResize({
|
|||||||
isResizingRef.current = false
|
isResizingRef.current = false
|
||||||
activeDirectionRef.current = null
|
activeDirectionRef.current = null
|
||||||
|
|
||||||
// Remove cursor from body
|
|
||||||
document.body.style.cursor = ''
|
document.body.style.cursor = ''
|
||||||
document.body.style.userSelect = ''
|
document.body.style.userSelect = ''
|
||||||
setCursor('')
|
setCursor('')
|
||||||
|
|||||||
@@ -3,6 +3,31 @@ import { and, eq, gt, gte, inArray, lt, lte, ne, type SQL, sql } from 'drizzle-o
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import type { TimeRange } from '@/stores/logs/filters/types'
|
import type { TimeRange } from '@/stores/logs/filters/types'
|
||||||
|
|
||||||
|
interface FilterValues {
|
||||||
|
timeRange: string
|
||||||
|
level: string
|
||||||
|
workflowIds: string[]
|
||||||
|
folderIds: string[]
|
||||||
|
triggers: string[]
|
||||||
|
searchQuery: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if any filters are currently active.
|
||||||
|
* @param filters - Current filter values
|
||||||
|
* @returns True if any filter is active
|
||||||
|
*/
|
||||||
|
export function hasActiveFilters(filters: FilterValues): boolean {
|
||||||
|
return (
|
||||||
|
filters.timeRange !== 'All time' ||
|
||||||
|
filters.level !== 'all' ||
|
||||||
|
filters.workflowIds.length > 0 ||
|
||||||
|
filters.folderIds.length > 0 ||
|
||||||
|
filters.triggers.length > 0 ||
|
||||||
|
filters.searchQuery.trim() !== ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared schema for log filter parameters.
|
* Shared schema for log filter parameters.
|
||||||
* Used by both the logs list API and export API.
|
* Used by both the logs list API and export API.
|
||||||
|
|||||||
@@ -51,11 +51,10 @@ export interface ExecutionEnvironment {
|
|||||||
workspaceId: string
|
workspaceId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALL_TRIGGER_TYPES = ['api', 'webhook', 'schedule', 'manual', 'chat', 'mcp'] as const
|
import type { CoreTriggerType } from '@/stores/logs/filters/types'
|
||||||
export type TriggerType = (typeof ALL_TRIGGER_TYPES)[number]
|
|
||||||
|
|
||||||
export interface ExecutionTrigger {
|
export interface ExecutionTrigger {
|
||||||
type: TriggerType | string
|
type: CoreTriggerType | string
|
||||||
source: string
|
source: string
|
||||||
data?: Record<string, unknown>
|
data?: Record<string, unknown>
|
||||||
timestamp: string
|
timestamp: string
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { FilterState, LogLevel, TimeRange, TriggerType } from '@/stores/logs/filters/types'
|
import {
|
||||||
|
CORE_TRIGGER_TYPES,
|
||||||
|
type FilterState,
|
||||||
|
type LogLevel,
|
||||||
|
type TimeRange,
|
||||||
|
type TriggerType,
|
||||||
|
} from '@/stores/logs/filters/types'
|
||||||
|
|
||||||
const getSearchParams = () => {
|
const getSearchParams = () => {
|
||||||
if (typeof window === 'undefined') return new URLSearchParams()
|
if (typeof window === 'undefined') return new URLSearchParams()
|
||||||
@@ -59,9 +65,7 @@ const parseTriggerArrayFromURL = (value: string | null): TriggerType[] => {
|
|||||||
if (!value) return []
|
if (!value) return []
|
||||||
return value
|
return value
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter((t): t is TriggerType =>
|
.filter((t): t is TriggerType => (CORE_TRIGGER_TYPES as readonly string[]).includes(t))
|
||||||
['chat', 'api', 'webhook', 'manual', 'schedule', 'mcp'].includes(t)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseStringArrayFromURL = (value: string | null): string[] => {
|
const parseStringArrayFromURL = (value: string | null): string[] => {
|
||||||
@@ -251,6 +255,22 @@ export const useFilterStore = create<FilterState>((set, get) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetFilters: () => {
|
||||||
|
set({
|
||||||
|
timeRange: DEFAULT_TIME_RANGE,
|
||||||
|
startDate: undefined,
|
||||||
|
endDate: undefined,
|
||||||
|
level: 'all',
|
||||||
|
workflowIds: [],
|
||||||
|
folderIds: [],
|
||||||
|
triggers: [],
|
||||||
|
searchQuery: '',
|
||||||
|
})
|
||||||
|
if (!get().isInitializing) {
|
||||||
|
get().syncWithURL()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
syncWithURL: () => {
|
syncWithURL: () => {
|
||||||
const { timeRange, startDate, endDate, level, workflowIds, folderIds, triggers, searchQuery } =
|
const { timeRange, startDate, endDate, level, workflowIds, folderIds, triggers, searchQuery } =
|
||||||
get()
|
get()
|
||||||
|
|||||||
@@ -173,15 +173,12 @@ export type TimeRange =
|
|||||||
| 'Custom range'
|
| 'Custom range'
|
||||||
|
|
||||||
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {})
|
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {})
|
||||||
export type TriggerType =
|
/** Core trigger types for workflow execution */
|
||||||
| 'chat'
|
export const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
||||||
| 'api'
|
|
||||||
| 'webhook'
|
export type CoreTriggerType = (typeof CORE_TRIGGER_TYPES)[number]
|
||||||
| 'manual'
|
|
||||||
| 'schedule'
|
export type TriggerType = CoreTriggerType | 'all' | (string & {})
|
||||||
| 'mcp'
|
|
||||||
| 'all'
|
|
||||||
| (string & {})
|
|
||||||
|
|
||||||
/** Filter state for logs and dashboard views */
|
/** Filter state for logs and dashboard views */
|
||||||
export interface FilterState {
|
export interface FilterState {
|
||||||
@@ -212,4 +209,5 @@ export interface FilterState {
|
|||||||
toggleTrigger: (trigger: TriggerType) => void
|
toggleTrigger: (trigger: TriggerType) => void
|
||||||
initializeFromURL: () => void
|
initializeFromURL: () => void
|
||||||
syncWithURL: () => void
|
syncWithURL: () => void
|
||||||
|
resetFilters: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user