mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -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'
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
1. React/core libraries
|
||||
|
||||
@@ -52,7 +52,7 @@ 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
|
||||
1. React/core libraries
|
||||
|
||||
@@ -12,7 +12,6 @@ import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||
import { processInputFileFields } from '@/lib/execution/files'
|
||||
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
||||
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 { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
|
||||
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 { StreamingExecution } from '@/executor/types'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||
import type { SubflowType } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('WorkflowExecuteAPI')
|
||||
|
||||
const ExecuteWorkflowSchema = z.object({
|
||||
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(),
|
||||
useDraftState: z.boolean().optional(),
|
||||
input: z.any().optional(),
|
||||
|
||||
@@ -6,14 +6,14 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
||||
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'
|
||||
|
||||
const logger = createLogger('WorkspaceNotificationAPI')
|
||||
|
||||
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([
|
||||
'consecutive_failures',
|
||||
|
||||
@@ -7,15 +7,15 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
||||
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'
|
||||
|
||||
const logger = createLogger('WorkspaceNotificationsAPI')
|
||||
|
||||
const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
|
||||
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([
|
||||
'consecutive_failures',
|
||||
@@ -81,7 +81,7 @@ const createNotificationSchema = z
|
||||
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
|
||||
allWorkflows: z.boolean().default(false),
|
||||
levelFilter: levelFilterSchema.default(['info', 'error']),
|
||||
triggerFilter: triggerFilterSchema.default([...ALL_TRIGGER_TYPES]),
|
||||
triggerFilter: triggerFilterSchema.default([...CORE_TRIGGER_TYPES]),
|
||||
includeFinalOutput: z.boolean().default(false),
|
||||
includeTraceSpans: 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 { FrozenCanvas } from './log-details/components/frozen-canvas'
|
||||
export { TraceSpans } from './log-details/components/trace-spans'
|
||||
export { LogRowContextMenu } from './log-row-context-menu'
|
||||
export { LogsList } from './logs-list'
|
||||
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,
|
||||
formatDuration,
|
||||
getDisplayStatus,
|
||||
LOG_COLUMNS,
|
||||
StatusBadge,
|
||||
TriggerBadge,
|
||||
} from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
@@ -21,6 +22,7 @@ interface LogRowProps {
|
||||
log: WorkflowLog
|
||||
isSelected: boolean
|
||||
onClick: (log: WorkflowLog) => void
|
||||
onContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
|
||||
}
|
||||
|
||||
@@ -29,11 +31,21 @@ interface LogRowProps {
|
||||
* Uses shallow comparison for the log object.
|
||||
*/
|
||||
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 handleClick = useCallback(() => onClick(log), [onClick, log])
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (onContextMenu) {
|
||||
e.preventDefault()
|
||||
onContextMenu(e, log)
|
||||
}
|
||||
},
|
||||
[onContextMenu, log]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isSelected ? selectedRowRef : null}
|
||||
@@ -42,25 +54,28 @@ const LogRow = memo(
|
||||
isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<div className='flex flex-1 items-center'>
|
||||
{/* Date */}
|
||||
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
<span
|
||||
className={`${LOG_COLUMNS.date.width} ${LOG_COLUMNS.date.minWidth} font-medium text-[12px] text-[var(--text-primary)]`}
|
||||
>
|
||||
{formattedDate.compactDate}
|
||||
</span>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
<span
|
||||
className={`${LOG_COLUMNS.time.width} ${LOG_COLUMNS.time.minWidth} font-medium text-[12px] text-[var(--text-primary)]`}
|
||||
>
|
||||
{formattedDate.compactTime}
|
||||
</span>
|
||||
|
||||
{/* Status */}
|
||||
<div className='w-[12%] min-w-[100px]'>
|
||||
<div className={`${LOG_COLUMNS.status.width} ${LOG_COLUMNS.status.minWidth}`}>
|
||||
<StatusBadge status={getDisplayStatus(log.status)} />
|
||||
</div>
|
||||
|
||||
{/* Workflow */}
|
||||
<div className='flex w-[22%] min-w-[140px] items-center gap-[8px] pr-[8px]'>
|
||||
<div
|
||||
className={`flex ${LOG_COLUMNS.workflow.width} ${LOG_COLUMNS.workflow.minWidth} items-center gap-[8px] pr-[8px]`}
|
||||
>
|
||||
<div
|
||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: log.workflow?.color }}
|
||||
@@ -70,13 +85,13 @@ const LogRow = memo(
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Cost */}
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
|
||||
<span
|
||||
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)}` : '—'}
|
||||
</span>
|
||||
|
||||
{/* Trigger */}
|
||||
<div className='w-[14%] min-w-[110px]'>
|
||||
<div className={`${LOG_COLUMNS.trigger.width} ${LOG_COLUMNS.trigger.minWidth}`}>
|
||||
{log.trigger ? (
|
||||
<TriggerBadge trigger={log.trigger} />
|
||||
) : (
|
||||
@@ -84,8 +99,7 @@ const LogRow = memo(
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className='w-[20%] min-w-[100px]'>
|
||||
<div className={`${LOG_COLUMNS.duration.width} ${LOG_COLUMNS.duration.minWidth}`}>
|
||||
<Badge variant='default' className='rounded-[6px] px-[9px] py-[2px] text-[12px]'>
|
||||
{formatDuration(log.duration) || '—'}
|
||||
</Badge>
|
||||
@@ -125,6 +139,7 @@ interface RowProps {
|
||||
logs: WorkflowLog[]
|
||||
selectedLogId: string | null
|
||||
onLogClick: (log: WorkflowLog) => void
|
||||
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||
isFetchingNextPage: boolean
|
||||
loaderRef: React.RefObject<HTMLDivElement | null>
|
||||
@@ -140,11 +155,11 @@ function Row({
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
onLogContextMenu,
|
||||
selectedRowRef,
|
||||
isFetchingNextPage,
|
||||
loaderRef,
|
||||
}: RowComponentProps<RowProps>) {
|
||||
// Show loader for the last item if loading more
|
||||
if (index >= logs.length) {
|
||||
return (
|
||||
<div style={style} className='flex items-center justify-center'>
|
||||
@@ -171,6 +186,7 @@ function Row({
|
||||
log={log}
|
||||
isSelected={isSelected}
|
||||
onClick={onLogClick}
|
||||
onContextMenu={onLogContextMenu}
|
||||
selectedRowRef={isSelected ? selectedRowRef : null}
|
||||
/>
|
||||
</div>
|
||||
@@ -181,6 +197,7 @@ export interface LogsListProps {
|
||||
logs: WorkflowLog[]
|
||||
selectedLogId: string | null
|
||||
onLogClick: (log: WorkflowLog) => void
|
||||
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||
hasNextPage: boolean
|
||||
isFetchingNextPage: boolean
|
||||
@@ -198,6 +215,7 @@ export function LogsList({
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
onLogContextMenu,
|
||||
selectedRowRef,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
@@ -208,7 +226,6 @@ export function LogsList({
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [listHeight, setListHeight] = useState(400)
|
||||
|
||||
// Measure container height for virtualization
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
@@ -226,7 +243,6 @@ export function LogsList({
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
|
||||
// Handle infinite scroll when nearing the end of the list
|
||||
const handleRowsRendered = useCallback(
|
||||
({ stopIndex }: { startIndex: number; stopIndex: number }) => {
|
||||
const threshold = logs.length - 10
|
||||
@@ -237,20 +253,27 @@ export function LogsList({
|
||||
[logs.length, hasNextPage, isFetchingNextPage, onLoadMore]
|
||||
)
|
||||
|
||||
// Calculate total item count including loader row
|
||||
const itemCount = hasNextPage ? logs.length + 1 : logs.length
|
||||
|
||||
// Row props passed to each row component
|
||||
const rowProps = useMemo<RowProps>(
|
||||
() => ({
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
onLogContextMenu,
|
||||
selectedRowRef,
|
||||
isFetchingNextPage,
|
||||
loaderRef,
|
||||
}),
|
||||
[logs, selectedLogId, onLogClick, selectedRowRef, isFetchingNextPage, loaderRef]
|
||||
[
|
||||
logs,
|
||||
selectedLogId,
|
||||
onLogClick,
|
||||
onLogContextMenu,
|
||||
selectedRowRef,
|
||||
isFetchingNextPage,
|
||||
loaderRef,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { SlackIcon } from '@/components/icons'
|
||||
import { Skeleton } from '@/components/ui'
|
||||
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 {
|
||||
type NotificationSubscription,
|
||||
@@ -34,6 +33,7 @@ import {
|
||||
} from '@/hooks/queries/notifications'
|
||||
import { useConnectOAuthService } from '@/hooks/queries/oauth-connections'
|
||||
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 { WorkflowSelector } from './components/workflow-selector'
|
||||
|
||||
@@ -133,7 +133,7 @@ export function NotificationSettings({
|
||||
workflowIds: [] as string[],
|
||||
allWorkflows: true,
|
||||
levelFilter: ['info', 'error'] as LogLevel[],
|
||||
triggerFilter: [...ALL_TRIGGER_TYPES] as TriggerType[],
|
||||
triggerFilter: [...CORE_TRIGGER_TYPES] as CoreTriggerType[],
|
||||
includeFinalOutput: false,
|
||||
includeTraceSpans: false,
|
||||
includeRateLimits: false,
|
||||
@@ -203,7 +203,7 @@ export function NotificationSettings({
|
||||
workflowIds: [],
|
||||
allWorkflows: true,
|
||||
levelFilter: ['info', 'error'],
|
||||
triggerFilter: [...ALL_TRIGGER_TYPES],
|
||||
triggerFilter: [...CORE_TRIGGER_TYPES],
|
||||
includeFinalOutput: false,
|
||||
includeTraceSpans: false,
|
||||
includeRateLimits: false,
|
||||
@@ -516,7 +516,7 @@ export function NotificationSettings({
|
||||
workflowIds: subscription.workflowIds || [],
|
||||
allWorkflows: subscription.allWorkflows,
|
||||
levelFilter: subscription.levelFilter as LogLevel[],
|
||||
triggerFilter: subscription.triggerFilter as TriggerType[],
|
||||
triggerFilter: subscription.triggerFilter as CoreTriggerType[],
|
||||
includeFinalOutput: subscription.includeFinalOutput,
|
||||
includeTraceSpans: subscription.includeTraceSpans,
|
||||
includeRateLimits: subscription.includeRateLimits,
|
||||
@@ -849,14 +849,14 @@ export function NotificationSettings({
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label className='text-[var(--text-secondary)]'>Trigger Type Filters</Label>
|
||||
<Combobox
|
||||
options={ALL_TRIGGER_TYPES.map((trigger) => ({
|
||||
options={CORE_TRIGGER_TYPES.map((trigger) => ({
|
||||
label: trigger.charAt(0).toUpperCase() + trigger.slice(1),
|
||||
value: trigger,
|
||||
}))}
|
||||
multiSelect
|
||||
multiSelectValues={formData.triggerFilter}
|
||||
onMultiSelectChange={(values) => {
|
||||
setFormData({ ...formData, triggerFilter: values as TriggerType[] })
|
||||
setFormData({ ...formData, triggerFilter: values as CoreTriggerType[] })
|
||||
setFormErrors({ ...formErrors, triggerFilter: '' })
|
||||
}}
|
||||
placeholder='Select trigger types...'
|
||||
|
||||
@@ -17,15 +17,15 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { DatePicker } from '@/components/emcn/components/date-picker/date-picker'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { hasActiveFilters } from '@/lib/logs/filters'
|
||||
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { useFolderStore } from '@/stores/folders/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 { AutocompleteSearch } from './components/search'
|
||||
|
||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
||||
|
||||
const TIME_RANGE_OPTIONS: ComboboxOption[] = [
|
||||
{ value: 'All time', label: 'All time' },
|
||||
{ value: 'Past 30 minutes', label: 'Past 30 minutes' },
|
||||
@@ -182,6 +182,7 @@ export function LogsToolbar({
|
||||
endDate,
|
||||
setDateRange,
|
||||
clearDateRange,
|
||||
resetFilters,
|
||||
} = useFilterStore()
|
||||
|
||||
const [datePickerOpen, setDatePickerOpen] = useState(false)
|
||||
@@ -346,23 +347,23 @@ export function LogsToolbar({
|
||||
setDatePickerOpen(false)
|
||||
}, [timeRange, startDate, previousTimeRange, setTimeRange])
|
||||
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return (
|
||||
level !== 'all' ||
|
||||
workflowIds.length > 0 ||
|
||||
folderIds.length > 0 ||
|
||||
triggers.length > 0 ||
|
||||
timeRange !== 'All time'
|
||||
)
|
||||
}, [level, workflowIds, folderIds, triggers, timeRange])
|
||||
const filtersActive = useMemo(
|
||||
() =>
|
||||
hasActiveFilters({
|
||||
timeRange,
|
||||
level,
|
||||
workflowIds,
|
||||
folderIds,
|
||||
triggers,
|
||||
searchQuery,
|
||||
}),
|
||||
[timeRange, level, workflowIds, folderIds, triggers, searchQuery]
|
||||
)
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setLevel('all')
|
||||
setWorkflowIds([])
|
||||
setFolderIds([])
|
||||
setTriggers([])
|
||||
clearDateRange()
|
||||
}, [setLevel, setWorkflowIds, setFolderIds, setTriggers, clearDateRange])
|
||||
resetFilters()
|
||||
onSearchQueryChange('')
|
||||
}, [resetFilters, onSearchQueryChange])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-[19px]'>
|
||||
@@ -462,7 +463,7 @@ export function LogsToolbar({
|
||||
</div>
|
||||
<div className='ml-auto flex items-center gap-[8px]'>
|
||||
{/* Clear Filters Button */}
|
||||
{hasActiveFilters && (
|
||||
{filtersActive && (
|
||||
<Button
|
||||
variant='active'
|
||||
onClick={handleClearFilters}
|
||||
|
||||
@@ -4,7 +4,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
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 { useFolders } from '@/hooks/queries/folders'
|
||||
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 type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||
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 REFRESH_SPINNER_DURATION_MS = 1000 as const
|
||||
@@ -35,10 +47,12 @@ export default function Logs() {
|
||||
level,
|
||||
workflowIds,
|
||||
folderIds,
|
||||
setWorkflowIds,
|
||||
setSearchQuery: setStoreSearchQuery,
|
||||
triggers,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
resetFilters,
|
||||
} = useFilterStore()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,6 +85,11 @@ export default function Logs() {
|
||||
const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false)
|
||||
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(
|
||||
() => ({
|
||||
timeRange,
|
||||
@@ -216,6 +235,56 @@ export default function Logs() {
|
||||
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(() => {
|
||||
if (selectedRowRef.current) {
|
||||
selectedRowRef.current.scrollIntoView({
|
||||
@@ -400,27 +469,17 @@ export default function Logs() {
|
||||
{/* 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 items-center'>
|
||||
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Date
|
||||
</span>
|
||||
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Time
|
||||
</span>
|
||||
<span className='w-[12%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Status
|
||||
</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>
|
||||
{LOG_COLUMN_ORDER.map((key) => {
|
||||
const col = LOG_COLUMNS[key]
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className={`${col.width} ${col.minWidth} font-medium text-[12px] text-[var(--text-tertiary)]`}
|
||||
>
|
||||
{col.label}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -452,6 +511,7 @@ export default function Logs() {
|
||||
logs={logs}
|
||||
selectedLogId={selectedLog?.id ?? null}
|
||||
onLogClick={handleLogClick}
|
||||
onLogContextMenu={handleLogContextMenu}
|
||||
selectedRowRef={selectedRowRef}
|
||||
hasNextPage={logsQuery.hasNextPage ?? false}
|
||||
isFetchingNextPage={logsQuery.isFetchingNextPage}
|
||||
@@ -481,6 +541,20 @@ export default function Logs() {
|
||||
open={isNotificationSettingsOpen}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,32 @@ import { format } from 'date-fns'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
||||
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 */
|
||||
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
|
||||
|
||||
@@ -24,11 +24,9 @@ export function useFloatBoundarySync({
|
||||
const positionRef = useRef(position)
|
||||
const previousDimensionsRef = useRef({ sidebarWidth: 0, panelWidth: 0, terminalHeight: 0 })
|
||||
|
||||
// Keep position ref up to date
|
||||
positionRef.current = position
|
||||
|
||||
const checkAndUpdatePosition = useCallback(() => {
|
||||
// Get current layout dimensions
|
||||
const sidebarWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||
)
|
||||
@@ -39,20 +37,17 @@ export function useFloatBoundarySync({
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
|
||||
)
|
||||
|
||||
// Check if dimensions actually changed
|
||||
const prev = previousDimensionsRef.current
|
||||
if (
|
||||
prev.sidebarWidth === sidebarWidth &&
|
||||
prev.panelWidth === panelWidth &&
|
||||
prev.terminalHeight === terminalHeight
|
||||
) {
|
||||
return // No change, skip update
|
||||
return
|
||||
}
|
||||
|
||||
// Update previous dimensions
|
||||
previousDimensionsRef.current = { sidebarWidth, panelWidth, terminalHeight }
|
||||
|
||||
// Calculate bounds
|
||||
const minX = sidebarWidth
|
||||
const maxX = window.innerWidth - panelWidth - width
|
||||
const minY = 0
|
||||
@@ -60,9 +55,7 @@ export function useFloatBoundarySync({
|
||||
|
||||
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) {
|
||||
// Constrain to new bounds
|
||||
const newPosition = {
|
||||
x: Math.max(minX, Math.min(maxX, currentPos.x)),
|
||||
y: Math.max(minY, Math.min(maxY, currentPos.y)),
|
||||
@@ -75,30 +68,24 @@ export function useFloatBoundarySync({
|
||||
if (!isOpen) return
|
||||
|
||||
const handleResize = () => {
|
||||
// Cancel any pending animation frame
|
||||
if (rafIdRef.current !== null) {
|
||||
cancelAnimationFrame(rafIdRef.current)
|
||||
}
|
||||
|
||||
// Schedule update on next animation frame for smooth 60fps updates
|
||||
rafIdRef.current = requestAnimationFrame(() => {
|
||||
checkAndUpdatePosition()
|
||||
rafIdRef.current = null
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for window resize
|
||||
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)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
})
|
||||
|
||||
// Initial check
|
||||
checkAndUpdatePosition()
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -22,7 +22,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
||||
*/
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Only left click
|
||||
if (e.button !== 0) return
|
||||
|
||||
e.preventDefault()
|
||||
@@ -32,7 +31,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
||||
dragStartRef.current = { x: e.clientX, y: e.clientY }
|
||||
initialPositionRef.current = { ...position }
|
||||
|
||||
// Add dragging cursor to body
|
||||
document.body.style.cursor = 'grabbing'
|
||||
document.body.style.userSelect = 'none'
|
||||
},
|
||||
@@ -54,7 +52,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
||||
y: initialPositionRef.current.y + deltaY,
|
||||
}
|
||||
|
||||
// Constrain to bounds
|
||||
const constrainedPosition = constrainChatPosition(newPosition, width, height)
|
||||
onPositionChange(constrainedPosition)
|
||||
},
|
||||
@@ -69,7 +66,6 @@ export function useFloatDrag({ position, width, height, onPositionChange }: UseF
|
||||
|
||||
isDraggingRef.current = false
|
||||
|
||||
// Remove dragging cursor
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}, [])
|
||||
|
||||
@@ -84,13 +84,11 @@ export function useFloatResize({
|
||||
const isNearLeft = x <= 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 && isNearRight) return 'top-right'
|
||||
if (isNearBottom && isNearLeft) return 'bottom-left'
|
||||
if (isNearBottom && isNearRight) return 'bottom-right'
|
||||
|
||||
// Check edges
|
||||
if (isNearTop) return 'top'
|
||||
if (isNearBottom) return 'bottom'
|
||||
if (isNearLeft) return 'left'
|
||||
@@ -155,7 +153,6 @@ export function useFloatResize({
|
||||
*/
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Only left click
|
||||
if (e.button !== 0) return
|
||||
|
||||
const chatElement = e.currentTarget as HTMLElement
|
||||
@@ -176,7 +173,6 @@ export function useFloatResize({
|
||||
height,
|
||||
}
|
||||
|
||||
// Set cursor on body
|
||||
document.body.style.cursor = getCursorForDirection(direction)
|
||||
document.body.style.userSelect = 'none'
|
||||
},
|
||||
@@ -195,7 +191,6 @@ export function useFloatResize({
|
||||
const initial = initialStateRef.current
|
||||
const direction = activeDirectionRef.current
|
||||
|
||||
// Get layout bounds
|
||||
const sidebarWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
|
||||
)
|
||||
@@ -206,18 +201,13 @@ export function useFloatResize({
|
||||
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') {
|
||||
// newY = initial.y + deltaY should never be less than 0
|
||||
const maxUpwardDelta = initial.y
|
||||
if (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') {
|
||||
const maxBottom = window.innerHeight - terminalHeight
|
||||
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') {
|
||||
const minLeft = sidebarWidth
|
||||
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') {
|
||||
const maxRight = window.innerWidth - panelWidth
|
||||
const initialRight = initial.x + initial.width
|
||||
@@ -256,9 +242,7 @@ export function useFloatResize({
|
||||
let newWidth = initial.width
|
||||
let newHeight = initial.height
|
||||
|
||||
// Calculate new dimensions based on resize direction
|
||||
switch (direction) {
|
||||
// Corners
|
||||
case 'top-left':
|
||||
newWidth = initial.width - deltaX
|
||||
newHeight = initial.height - deltaY
|
||||
@@ -280,7 +264,6 @@ export function useFloatResize({
|
||||
newHeight = initial.height + deltaY
|
||||
break
|
||||
|
||||
// Edges
|
||||
case 'top':
|
||||
newHeight = initial.height - deltaY
|
||||
newY = initial.y + deltaY
|
||||
@@ -297,8 +280,6 @@ export function useFloatResize({
|
||||
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 effectiveMaxWidth = typeof maxWidth === 'number' ? maxWidth : MAX_CHAT_WIDTH
|
||||
const effectiveMinHeight = typeof minHeight === 'number' ? minHeight : MIN_CHAT_HEIGHT
|
||||
@@ -310,7 +291,6 @@ export function useFloatResize({
|
||||
Math.min(effectiveMaxHeight, newHeight)
|
||||
)
|
||||
|
||||
// Adjust position if dimensions were constrained on left/top edges
|
||||
if (direction === 'top-left' || direction === 'bottom-left' || direction === 'left') {
|
||||
if (constrainedWidth !== newWidth) {
|
||||
newX = initial.x + initial.width - constrainedWidth
|
||||
@@ -322,7 +302,6 @@ export function useFloatResize({
|
||||
}
|
||||
}
|
||||
|
||||
// Constrain position to bounds
|
||||
const minX = sidebarWidth
|
||||
const maxX = window.innerWidth - panelWidth - constrainedWidth
|
||||
const minY = 0
|
||||
@@ -331,7 +310,6 @@ export function useFloatResize({
|
||||
const finalX = Math.max(minX, Math.min(maxX, newX))
|
||||
const finalY = Math.max(minY, Math.min(maxY, newY))
|
||||
|
||||
// Update state
|
||||
onDimensionsChange({
|
||||
width: constrainedWidth,
|
||||
height: constrainedHeight,
|
||||
@@ -353,7 +331,6 @@ export function useFloatResize({
|
||||
isResizingRef.current = false
|
||||
activeDirectionRef.current = null
|
||||
|
||||
// Remove cursor from body
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
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 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.
|
||||
* Used by both the logs list API and export API.
|
||||
|
||||
@@ -51,11 +51,10 @@ export interface ExecutionEnvironment {
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
export const ALL_TRIGGER_TYPES = ['api', 'webhook', 'schedule', 'manual', 'chat', 'mcp'] as const
|
||||
export type TriggerType = (typeof ALL_TRIGGER_TYPES)[number]
|
||||
import type { CoreTriggerType } from '@/stores/logs/filters/types'
|
||||
|
||||
export interface ExecutionTrigger {
|
||||
type: TriggerType | string
|
||||
type: CoreTriggerType | string
|
||||
source: string
|
||||
data?: Record<string, unknown>
|
||||
timestamp: string
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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 = () => {
|
||||
if (typeof window === 'undefined') return new URLSearchParams()
|
||||
@@ -59,9 +65,7 @@ const parseTriggerArrayFromURL = (value: string | null): TriggerType[] => {
|
||||
if (!value) return []
|
||||
return value
|
||||
.split(',')
|
||||
.filter((t): t is TriggerType =>
|
||||
['chat', 'api', 'webhook', 'manual', 'schedule', 'mcp'].includes(t)
|
||||
)
|
||||
.filter((t): t is TriggerType => (CORE_TRIGGER_TYPES as readonly string[]).includes(t))
|
||||
}
|
||||
|
||||
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: () => {
|
||||
const { timeRange, startDate, endDate, level, workflowIds, folderIds, triggers, searchQuery } =
|
||||
get()
|
||||
|
||||
@@ -173,15 +173,12 @@ export type TimeRange =
|
||||
| 'Custom range'
|
||||
|
||||
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {})
|
||||
export type TriggerType =
|
||||
| 'chat'
|
||||
| 'api'
|
||||
| 'webhook'
|
||||
| 'manual'
|
||||
| 'schedule'
|
||||
| 'mcp'
|
||||
| 'all'
|
||||
| (string & {})
|
||||
/** Core trigger types for workflow execution */
|
||||
export const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
||||
|
||||
export type CoreTriggerType = (typeof CORE_TRIGGER_TYPES)[number]
|
||||
|
||||
export type TriggerType = CoreTriggerType | 'all' | (string & {})
|
||||
|
||||
/** Filter state for logs and dashboard views */
|
||||
export interface FilterState {
|
||||
@@ -212,4 +209,5 @@ export interface FilterState {
|
||||
toggleTrigger: (trigger: TriggerType) => void
|
||||
initializeFromURL: () => void
|
||||
syncWithURL: () => void
|
||||
resetFilters: () => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user