mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-15 00:44:56 -05:00
Compare commits
56 Commits
fix/logs-l
...
v0.5.89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b45f3962fc | ||
|
|
07d50f8fe1 | ||
|
|
27973953f6 | ||
|
|
50585273ce | ||
|
|
654cb2b407 | ||
|
|
6c66521d64 | ||
|
|
479cd347ad | ||
|
|
a3a99eda19 | ||
|
|
1a66d48add | ||
|
|
46822e91f3 | ||
|
|
2bb68335ee | ||
|
|
8528fbe2d2 | ||
|
|
31fdd2be13 | ||
|
|
028bc652c2 | ||
|
|
c6bf5cd58c | ||
|
|
11dc18a80d | ||
|
|
ab4e9dc72f | ||
|
|
1c58c35bd8 | ||
|
|
d63a5cb504 | ||
|
|
8bd5d41723 | ||
|
|
c12931bc50 | ||
|
|
e9c4251c1c | ||
|
|
cc2be33d6b | ||
|
|
45371e521e | ||
|
|
0ce0f98aa5 | ||
|
|
dff1c9d083 | ||
|
|
b09f683072 | ||
|
|
a8bb0db660 | ||
|
|
af82820a28 | ||
|
|
4372841797 | ||
|
|
5e8c843241 | ||
|
|
7bf3d73ee6 | ||
|
|
7ffc11a738 | ||
|
|
be578e2ed7 | ||
|
|
f415e5edc4 | ||
|
|
13a6e6c3fa | ||
|
|
f5ab7f21ae | ||
|
|
bfb6fffe38 | ||
|
|
4fbec0a43f | ||
|
|
585f5e365b | ||
|
|
3792bdd252 | ||
|
|
eb5d1f3e5b | ||
|
|
54ab82c8dd | ||
|
|
f895bf469b | ||
|
|
dd3209af06 | ||
|
|
b6ba3b50a7 | ||
|
|
b304233062 | ||
|
|
57e4b49bd6 | ||
|
|
e12dd204ed | ||
|
|
3d9d9cbc54 | ||
|
|
0f4ec962ad | ||
|
|
4827866f9a | ||
|
|
3e697d9ed9 | ||
|
|
4431a1a484 | ||
|
|
4d1a9a3f22 | ||
|
|
eb07a080fb |
@@ -1,2 +1,2 @@
|
|||||||
export type { StatusBarSegment } from './status-bar'
|
export type { StatusBarSegment } from './status-bar'
|
||||||
export { StatusBar } from './status-bar'
|
export { default, StatusBar } from './status-bar'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface StatusBarSegment {
|
|||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusBarInner({
|
export function StatusBar({
|
||||||
segments,
|
segments,
|
||||||
selectedSegmentIndices,
|
selectedSegmentIndices,
|
||||||
onSegmentClick,
|
onSegmentClick,
|
||||||
@@ -127,45 +127,4 @@ function StatusBarInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default memo(StatusBar)
|
||||||
* Custom equality function for StatusBar memo.
|
|
||||||
* Performs structural comparison of segments array to avoid re-renders
|
|
||||||
* when poll data returns new object references with identical content.
|
|
||||||
*/
|
|
||||||
function areStatusBarPropsEqual(
|
|
||||||
prev: Parameters<typeof StatusBarInner>[0],
|
|
||||||
next: Parameters<typeof StatusBarInner>[0]
|
|
||||||
): boolean {
|
|
||||||
if (prev.workflowId !== next.workflowId) return false
|
|
||||||
if (prev.segmentDurationMs !== next.segmentDurationMs) return false
|
|
||||||
if (prev.preferBelow !== next.preferBelow) return false
|
|
||||||
|
|
||||||
if (prev.selectedSegmentIndices !== next.selectedSegmentIndices) {
|
|
||||||
if (!prev.selectedSegmentIndices || !next.selectedSegmentIndices) return false
|
|
||||||
if (prev.selectedSegmentIndices.length !== next.selectedSegmentIndices.length) return false
|
|
||||||
for (let i = 0; i < prev.selectedSegmentIndices.length; i++) {
|
|
||||||
if (prev.selectedSegmentIndices[i] !== next.selectedSegmentIndices[i]) return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev.segments !== next.segments) {
|
|
||||||
if (prev.segments.length !== next.segments.length) return false
|
|
||||||
for (let i = 0; i < prev.segments.length; i++) {
|
|
||||||
const ps = prev.segments[i]
|
|
||||||
const ns = next.segments[i]
|
|
||||||
if (
|
|
||||||
ps.successRate !== ns.successRate ||
|
|
||||||
ps.hasExecutions !== ns.hasExecutions ||
|
|
||||||
ps.totalExecutions !== ns.totalExecutions ||
|
|
||||||
ps.successfulExecutions !== ns.successfulExecutions ||
|
|
||||||
ps.timestamp !== ns.timestamp
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StatusBar = memo(StatusBarInner, areStatusBarPropsEqual)
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export type { WorkflowExecutionItem } from './workflows-list'
|
export type { WorkflowExecutionItem } from './workflows-list'
|
||||||
export { WorkflowsList } from './workflows-list'
|
export { default, WorkflowsList } from './workflows-list'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface WorkflowExecutionItem {
|
|||||||
overallSuccessRate: number
|
overallSuccessRate: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkflowsListInner({
|
export function WorkflowsList({
|
||||||
filteredExecutions,
|
filteredExecutions,
|
||||||
expandedWorkflowId,
|
expandedWorkflowId,
|
||||||
onToggleWorkflow,
|
onToggleWorkflow,
|
||||||
@@ -103,7 +103,7 @@ function WorkflowsListInner({
|
|||||||
<StatusBar
|
<StatusBar
|
||||||
segments={workflow.segments}
|
segments={workflow.segments}
|
||||||
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
|
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
|
||||||
onSegmentClick={onSegmentClick}
|
onSegmentClick={onSegmentClick as any}
|
||||||
workflowId={workflow.workflowId}
|
workflowId={workflow.workflowId}
|
||||||
segmentDurationMs={segmentDurationMs}
|
segmentDurationMs={segmentDurationMs}
|
||||||
preferBelow={idx < 2}
|
preferBelow={idx < 2}
|
||||||
@@ -124,4 +124,4 @@ function WorkflowsListInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkflowsList = memo(WorkflowsListInner)
|
export default memo(WorkflowsList)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
|
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||||
@@ -141,10 +141,10 @@ function toWorkflowExecution(wf: WorkflowStats): WorkflowExecution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
|
||||||
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
|
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
|
||||||
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
|
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
|
||||||
const lastAnchorIndicesRef = useRef<Record<string, number>>({})
|
const barsAreaRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore()
|
const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore()
|
||||||
|
|
||||||
@@ -152,79 +152,20 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
|||||||
|
|
||||||
const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null
|
const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null
|
||||||
|
|
||||||
const { rawExecutions, aggregateSegments, segmentMs } = useMemo(() => {
|
const { executions, aggregateSegments, segmentMs } = useMemo(() => {
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return { rawExecutions: [], aggregateSegments: [], segmentMs: 0 }
|
return { executions: [], aggregateSegments: [], segmentMs: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflowExecutions = stats.workflows.map(toWorkflowExecution)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rawExecutions: stats.workflows.map(toWorkflowExecution),
|
executions: workflowExecutions,
|
||||||
aggregateSegments: stats.aggregateSegments,
|
aggregateSegments: stats.aggregateSegments,
|
||||||
segmentMs: stats.segmentMs,
|
segmentMs: stats.segmentMs,
|
||||||
}
|
}
|
||||||
}, [stats])
|
}, [stats])
|
||||||
|
|
||||||
/**
|
|
||||||
* Stabilize execution objects: reuse previous references for workflows
|
|
||||||
* whose segment data hasn't structurally changed between polls.
|
|
||||||
* This prevents cascading re-renders through WorkflowsList → StatusBar.
|
|
||||||
*/
|
|
||||||
const prevExecutionsRef = useRef<WorkflowExecution[]>([])
|
|
||||||
|
|
||||||
const executions = useMemo(() => {
|
|
||||||
const prevMap = new Map(prevExecutionsRef.current.map((e) => [e.workflowId, e]))
|
|
||||||
let anyChanged = false
|
|
||||||
|
|
||||||
const result = rawExecutions.map((exec) => {
|
|
||||||
const prev = prevMap.get(exec.workflowId)
|
|
||||||
if (!prev) {
|
|
||||||
anyChanged = true
|
|
||||||
return exec
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
prev.overallSuccessRate !== exec.overallSuccessRate ||
|
|
||||||
prev.workflowName !== exec.workflowName ||
|
|
||||||
prev.segments.length !== exec.segments.length
|
|
||||||
) {
|
|
||||||
anyChanged = true
|
|
||||||
return exec
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < prev.segments.length; i++) {
|
|
||||||
const ps = prev.segments[i]
|
|
||||||
const ns = exec.segments[i]
|
|
||||||
if (
|
|
||||||
ps.totalExecutions !== ns.totalExecutions ||
|
|
||||||
ps.successfulExecutions !== ns.successfulExecutions ||
|
|
||||||
ps.timestamp !== ns.timestamp ||
|
|
||||||
ps.avgDurationMs !== ns.avgDurationMs ||
|
|
||||||
ps.p50Ms !== ns.p50Ms ||
|
|
||||||
ps.p90Ms !== ns.p90Ms ||
|
|
||||||
ps.p99Ms !== ns.p99Ms
|
|
||||||
) {
|
|
||||||
anyChanged = true
|
|
||||||
return exec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
|
||||||
!anyChanged &&
|
|
||||||
result.length === prevExecutionsRef.current.length &&
|
|
||||||
result.every((r, i) => r === prevExecutionsRef.current[i])
|
|
||||||
) {
|
|
||||||
return prevExecutionsRef.current
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}, [rawExecutions])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
prevExecutionsRef.current = executions
|
|
||||||
}, [executions])
|
|
||||||
|
|
||||||
const lastExecutionByWorkflow = useMemo(() => {
|
const lastExecutionByWorkflow = useMemo(() => {
|
||||||
const map = new Map<string, number>()
|
const map = new Map<string, number>()
|
||||||
for (const wf of executions) {
|
for (const wf of executions) {
|
||||||
@@ -371,8 +312,6 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
|||||||
[toggleWorkflowId]
|
[toggleWorkflowId]
|
||||||
)
|
)
|
||||||
|
|
||||||
lastAnchorIndicesRef.current = lastAnchorIndices
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles segment click for selecting time segments.
|
* Handles segment click for selecting time segments.
|
||||||
* @param workflowId - The workflow containing the segment
|
* @param workflowId - The workflow containing the segment
|
||||||
@@ -422,7 +361,7 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
|||||||
} else if (mode === 'range') {
|
} else if (mode === 'range') {
|
||||||
setSelectedSegments((prev) => {
|
setSelectedSegments((prev) => {
|
||||||
const currentSegments = prev[workflowId] || []
|
const currentSegments = prev[workflowId] || []
|
||||||
const anchor = lastAnchorIndicesRef.current[workflowId] ?? segmentIndex
|
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
|
||||||
const [start, end] =
|
const [start, end] =
|
||||||
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
|
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
|
||||||
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||||
@@ -431,12 +370,12 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[lastAnchorIndices]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedSegments((prev) => (Object.keys(prev).length > 0 ? {} : prev))
|
setSelectedSegments({})
|
||||||
setLastAnchorIndices((prev) => (Object.keys(prev).length > 0 ? {} : prev))
|
setLastAnchorIndices({})
|
||||||
}, [stats, timeRange, workflowIds, searchQuery])
|
}, [stats, timeRange, workflowIds, searchQuery])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -554,7 +493,7 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
<div className='min-h-0 flex-1 overflow-hidden' ref={barsAreaRef}>
|
||||||
<WorkflowsList
|
<WorkflowsList
|
||||||
filteredExecutions={filteredExecutions as WorkflowExecution[]}
|
filteredExecutions={filteredExecutions as WorkflowExecution[]}
|
||||||
expandedWorkflowId={expandedWorkflowId}
|
expandedWorkflowId={expandedWorkflowId}
|
||||||
@@ -568,5 +507,3 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(DashboardInner)
|
|
||||||
|
|||||||
@@ -43,12 +43,11 @@ import { useLogDetailsUIStore } from '@/stores/logs/store'
|
|||||||
/**
|
/**
|
||||||
* Workflow Output section with code viewer, copy, search, and context menu functionality
|
* Workflow Output section with code viewer, copy, search, and context menu functionality
|
||||||
*/
|
*/
|
||||||
const WorkflowOutputSection = memo(
|
function WorkflowOutputSection({ output }: { output: Record<string, unknown> }) {
|
||||||
function WorkflowOutputSection({ output }: { output: Record<string, unknown> }) {
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const copyTimerRef = useRef<number | null>(null)
|
|
||||||
|
|
||||||
|
// Context menu state
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
|
||||||
|
|
||||||
@@ -82,17 +81,10 @@ const WorkflowOutputSection = memo(
|
|||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(jsonString)
|
navigator.clipboard.writeText(jsonString)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current)
|
setTimeout(() => setCopied(false), 1500)
|
||||||
copyTimerRef.current = window.setTimeout(() => setCopied(false), 1500)
|
|
||||||
closeContextMenu()
|
closeContextMenu()
|
||||||
}, [jsonString, closeContextMenu])
|
}, [jsonString, closeContextMenu])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleSearch = useCallback(() => {
|
||||||
activateSearch()
|
activateSearch()
|
||||||
closeContextMenu()
|
closeContextMenu()
|
||||||
@@ -193,12 +185,7 @@ const WorkflowOutputSection = memo(
|
|||||||
>
|
>
|
||||||
<ArrowDown className='h-[12px] w-[12px]' />
|
<ArrowDown className='h-[12px] w-[12px]' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant='ghost' className='!p-1' onClick={closeSearch} aria-label='Close search'>
|
||||||
variant='ghost'
|
|
||||||
className='!p-1'
|
|
||||||
onClick={closeSearch}
|
|
||||||
aria-label='Close search'
|
|
||||||
>
|
|
||||||
<X className='h-[12px] w-[12px]' />
|
<X className='h-[12px] w-[12px]' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,9 +220,7 @@ const WorkflowOutputSection = memo(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
(prev, next) => prev.output === next.output
|
|
||||||
)
|
|
||||||
|
|
||||||
interface LogDetailsProps {
|
interface LogDetailsProps {
|
||||||
/** The log to display details for */
|
/** The log to display details for */
|
||||||
@@ -293,6 +278,7 @@ export const LogDetails = memo(function LogDetails({
|
|||||||
return isWorkflowExecutionLog && log?.cost
|
return isWorkflowExecutionLog && log?.cost
|
||||||
}, [log, isWorkflowExecutionLog])
|
}, [log, isWorkflowExecutionLog])
|
||||||
|
|
||||||
|
// Extract and clean the workflow final output (recursively remove hidden keys for cleaner display)
|
||||||
const workflowOutput = useMemo(() => {
|
const workflowOutput = useMemo(() => {
|
||||||
const executionData = log?.executionData as
|
const executionData = log?.executionData as
|
||||||
| { finalOutput?: Record<string, unknown> }
|
| { finalOutput?: Record<string, unknown> }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import { memo } from 'react'
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverAnchor,
|
PopoverAnchor,
|
||||||
@@ -30,7 +29,7 @@ interface LogRowContextMenuProps {
|
|||||||
* Context menu for log rows.
|
* Context menu for log rows.
|
||||||
* Provides quick actions for copying data, navigation, and filtering.
|
* Provides quick actions for copying data, navigation, and filtering.
|
||||||
*/
|
*/
|
||||||
export const LogRowContextMenu = memo(function LogRowContextMenu({
|
export function LogRowContextMenu({
|
||||||
isOpen,
|
isOpen,
|
||||||
position,
|
position,
|
||||||
menuRef,
|
menuRef,
|
||||||
@@ -122,4 +121,4 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ interface LogRowProps {
|
|||||||
log: WorkflowLog
|
log: WorkflowLog
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
onClick: (log: WorkflowLog) => void
|
onClick: (log: WorkflowLog) => void
|
||||||
onHover?: (log: WorkflowLog) => void
|
|
||||||
onContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
onContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
|
selectedRowRef: React.RefObject<HTMLTableRowElement | null> | null
|
||||||
}
|
}
|
||||||
@@ -34,14 +33,7 @@ interface LogRowProps {
|
|||||||
* Uses shallow comparison for the log object.
|
* Uses shallow comparison for the log object.
|
||||||
*/
|
*/
|
||||||
const LogRow = memo(
|
const LogRow = memo(
|
||||||
function LogRow({
|
function LogRow({ log, isSelected, onClick, onContextMenu, selectedRowRef }: LogRowProps) {
|
||||||
log,
|
|
||||||
isSelected,
|
|
||||||
onClick,
|
|
||||||
onHover,
|
|
||||||
onContextMenu,
|
|
||||||
selectedRowRef,
|
|
||||||
}: LogRowProps) {
|
|
||||||
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
||||||
const isDeletedWorkflow = !log.workflow?.id && !log.workflowId
|
const isDeletedWorkflow = !log.workflow?.id && !log.workflowId
|
||||||
const workflowName = isDeletedWorkflow
|
const workflowName = isDeletedWorkflow
|
||||||
@@ -51,8 +43,6 @@ const LogRow = memo(
|
|||||||
|
|
||||||
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => onHover?.(log), [onHover, log])
|
|
||||||
|
|
||||||
const handleContextMenu = useCallback(
|
const handleContextMenu = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (onContextMenu) {
|
if (onContextMenu) {
|
||||||
@@ -71,7 +61,6 @@ 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}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
<div className='flex flex-1 items-center'>
|
<div className='flex flex-1 items-center'>
|
||||||
@@ -153,8 +142,7 @@ const LogRow = memo(
|
|||||||
prevProps.log.id === nextProps.log.id &&
|
prevProps.log.id === nextProps.log.id &&
|
||||||
prevProps.log.duration === nextProps.log.duration &&
|
prevProps.log.duration === nextProps.log.duration &&
|
||||||
prevProps.log.status === nextProps.log.status &&
|
prevProps.log.status === nextProps.log.status &&
|
||||||
prevProps.isSelected === nextProps.isSelected &&
|
prevProps.isSelected === nextProps.isSelected
|
||||||
prevProps.onHover === nextProps.onHover
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -163,7 +151,6 @@ interface RowProps {
|
|||||||
logs: WorkflowLog[]
|
logs: WorkflowLog[]
|
||||||
selectedLogId: string | null
|
selectedLogId: string | null
|
||||||
onLogClick: (log: WorkflowLog) => void
|
onLogClick: (log: WorkflowLog) => void
|
||||||
onLogHover?: (log: WorkflowLog) => void
|
|
||||||
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||||
isFetchingNextPage: boolean
|
isFetchingNextPage: boolean
|
||||||
@@ -180,7 +167,6 @@ function Row({
|
|||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
onLogHover,
|
|
||||||
onLogContextMenu,
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
@@ -212,7 +198,6 @@ function Row({
|
|||||||
log={log}
|
log={log}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onClick={onLogClick}
|
onClick={onLogClick}
|
||||||
onHover={onLogHover}
|
|
||||||
onContextMenu={onLogContextMenu}
|
onContextMenu={onLogContextMenu}
|
||||||
selectedRowRef={isSelected ? selectedRowRef : null}
|
selectedRowRef={isSelected ? selectedRowRef : null}
|
||||||
/>
|
/>
|
||||||
@@ -224,7 +209,6 @@ export interface LogsListProps {
|
|||||||
logs: WorkflowLog[]
|
logs: WorkflowLog[]
|
||||||
selectedLogId: string | null
|
selectedLogId: string | null
|
||||||
onLogClick: (log: WorkflowLog) => void
|
onLogClick: (log: WorkflowLog) => void
|
||||||
onLogHover?: (log: WorkflowLog) => void
|
|
||||||
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
onLogContextMenu?: (e: React.MouseEvent, log: WorkflowLog) => void
|
||||||
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
selectedRowRef: React.RefObject<HTMLTableRowElement | null>
|
||||||
hasNextPage: boolean
|
hasNextPage: boolean
|
||||||
@@ -243,7 +227,6 @@ export function LogsList({
|
|||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
onLogHover,
|
|
||||||
onLogContextMenu,
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
@@ -289,7 +272,6 @@ export function LogsList({
|
|||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
onLogHover,
|
|
||||||
onLogContextMenu,
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
@@ -299,7 +281,6 @@ export function LogsList({
|
|||||||
logs,
|
logs,
|
||||||
selectedLogId,
|
selectedLogId,
|
||||||
onLogClick,
|
onLogClick,
|
||||||
onLogHover,
|
|
||||||
onLogContextMenu,
|
onLogContextMenu,
|
||||||
selectedRowRef,
|
selectedRowRef,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { Plus, X } from 'lucide-react'
|
import { Plus, X } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
@@ -113,7 +113,7 @@ function formatAlertConfigLabel(config: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationSettings = memo(function NotificationSettings({
|
export function NotificationSettings({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
@@ -144,7 +144,7 @@ export const NotificationSettings = memo(function NotificationSettings({
|
|||||||
slackChannelId: '',
|
slackChannelId: '',
|
||||||
slackChannelName: '',
|
slackChannelName: '',
|
||||||
slackAccountId: '',
|
slackAccountId: '',
|
||||||
|
useAlertRule: false,
|
||||||
alertRule: 'none' as AlertRule,
|
alertRule: 'none' as AlertRule,
|
||||||
consecutiveFailures: 3,
|
consecutiveFailures: 3,
|
||||||
failureRatePercent: 50,
|
failureRatePercent: 50,
|
||||||
@@ -212,7 +212,7 @@ export const NotificationSettings = memo(function NotificationSettings({
|
|||||||
slackChannelId: '',
|
slackChannelId: '',
|
||||||
slackChannelName: '',
|
slackChannelName: '',
|
||||||
slackAccountId: '',
|
slackAccountId: '',
|
||||||
|
useAlertRule: false,
|
||||||
alertRule: 'none',
|
alertRule: 'none',
|
||||||
consecutiveFailures: 3,
|
consecutiveFailures: 3,
|
||||||
failureRatePercent: 50,
|
failureRatePercent: 50,
|
||||||
@@ -484,6 +484,7 @@ export const NotificationSettings = memo(function NotificationSettings({
|
|||||||
slackChannelId: subscription.slackConfig?.channelId || '',
|
slackChannelId: subscription.slackConfig?.channelId || '',
|
||||||
slackChannelName: subscription.slackConfig?.channelName || '',
|
slackChannelName: subscription.slackConfig?.channelName || '',
|
||||||
slackAccountId: subscription.slackConfig?.accountId || '',
|
slackAccountId: subscription.slackConfig?.accountId || '',
|
||||||
|
useAlertRule: !!subscription.alertConfig,
|
||||||
alertRule: subscription.alertConfig?.rule || 'none',
|
alertRule: subscription.alertConfig?.rule || 'none',
|
||||||
consecutiveFailures: subscription.alertConfig?.consecutiveFailures || 3,
|
consecutiveFailures: subscription.alertConfig?.consecutiveFailures || 3,
|
||||||
failureRatePercent: subscription.alertConfig?.failureRatePercent || 50,
|
failureRatePercent: subscription.alertConfig?.failureRatePercent || 50,
|
||||||
@@ -1288,4 +1289,4 @@ export const NotificationSettings = memo(function NotificationSettings({
|
|||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { memo, useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { ArrowUp, Bell, Library, MoreHorizontal, RefreshCw } from 'lucide-react'
|
import { ArrowUp, Bell, Library, MoreHorizontal, RefreshCw } from 'lucide-react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import {
|
import {
|
||||||
@@ -149,7 +149,7 @@ function getTriggerIcon(
|
|||||||
* @param props - The component props
|
* @param props - The component props
|
||||||
* @returns The complete logs toolbar
|
* @returns The complete logs toolbar
|
||||||
*/
|
*/
|
||||||
export const LogsToolbar = memo(function LogsToolbar({
|
export function LogsToolbar({
|
||||||
viewMode,
|
viewMode,
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
@@ -749,4 +749,4 @@ export const LogsToolbar = memo(function LogsToolbar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
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'
|
||||||
@@ -11,17 +10,12 @@ import {
|
|||||||
hasActiveFilters,
|
hasActiveFilters,
|
||||||
} from '@/lib/logs/filters'
|
} from '@/lib/logs/filters'
|
||||||
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
||||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
|
||||||
import { useFolders } from '@/hooks/queries/folders'
|
import { useFolders } from '@/hooks/queries/folders'
|
||||||
import {
|
import { useDashboardStats, useLogDetail, useLogsList } from '@/hooks/queries/logs'
|
||||||
prefetchLogDetail,
|
|
||||||
useDashboardStats,
|
|
||||||
useLogDetail,
|
|
||||||
useLogsList,
|
|
||||||
} from '@/hooks/queries/logs'
|
|
||||||
import { useDebounce } from '@/hooks/use-debounce'
|
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 {
|
import {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
ExecutionSnapshot,
|
ExecutionSnapshot,
|
||||||
@@ -36,38 +30,6 @@ 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
|
||||||
|
|
||||||
interface LogSelectionState {
|
|
||||||
selectedLogId: string | null
|
|
||||||
isSidebarOpen: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogSelectionAction =
|
|
||||||
| { type: 'TOGGLE_LOG'; logId: string }
|
|
||||||
| { type: 'SELECT_LOG'; logId: string }
|
|
||||||
| { type: 'CLOSE_SIDEBAR' }
|
|
||||||
| { type: 'TOGGLE_SIDEBAR' }
|
|
||||||
|
|
||||||
function logSelectionReducer(
|
|
||||||
state: LogSelectionState,
|
|
||||||
action: LogSelectionAction
|
|
||||||
): LogSelectionState {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'TOGGLE_LOG':
|
|
||||||
if (state.selectedLogId === action.logId && state.isSidebarOpen) {
|
|
||||||
return { selectedLogId: null, isSidebarOpen: false }
|
|
||||||
}
|
|
||||||
return { selectedLogId: action.logId, isSidebarOpen: true }
|
|
||||||
case 'SELECT_LOG':
|
|
||||||
return { ...state, selectedLogId: action.logId }
|
|
||||||
case 'CLOSE_SIDEBAR':
|
|
||||||
return { selectedLogId: null, isSidebarOpen: false }
|
|
||||||
case 'TOGGLE_SIDEBAR':
|
|
||||||
return state.selectedLogId ? { ...state, isSidebarOpen: !state.isSidebarOpen } : state
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs page component displaying workflow execution history.
|
* Logs page component displaying workflow execution history.
|
||||||
* Supports filtering, search, live updates, and detailed log inspection.
|
* Supports filtering, search, live updates, and detailed log inspection.
|
||||||
@@ -98,13 +60,11 @@ export default function Logs() {
|
|||||||
setWorkspaceId(workspaceId)
|
setWorkspaceId(workspaceId)
|
||||||
}, [workspaceId, setWorkspaceId])
|
}, [workspaceId, setWorkspaceId])
|
||||||
|
|
||||||
const [{ selectedLogId, isSidebarOpen }, dispatch] = useReducer(logSelectionReducer, {
|
const [selectedLogId, setSelectedLogId] = useState<string | null>(null)
|
||||||
selectedLogId: null,
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
||||||
isSidebarOpen: false,
|
|
||||||
})
|
|
||||||
const selectedRowRef = useRef<HTMLTableRowElement | null>(null)
|
const selectedRowRef = useRef<HTMLTableRowElement | null>(null)
|
||||||
const loaderRef = useRef<HTMLDivElement>(null)
|
const loaderRef = useRef<HTMLDivElement>(null)
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const isInitialized = useRef<boolean>(false)
|
const isInitialized = useRef<boolean>(false)
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
@@ -122,13 +82,6 @@ export default function Logs() {
|
|||||||
const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false)
|
const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false)
|
||||||
const [isExporting, setIsExporting] = useState(false)
|
const [isExporting, setIsExporting] = useState(false)
|
||||||
const isSearchOpenRef = useRef<boolean>(false)
|
const isSearchOpenRef = useRef<boolean>(false)
|
||||||
const refreshTimersRef = useRef(new Set<number>())
|
|
||||||
const logsRef = useRef<WorkflowLog[]>([])
|
|
||||||
const selectedLogIndexRef = useRef(-1)
|
|
||||||
const selectedLogIdRef = useRef<string | null>(null)
|
|
||||||
const logsRefetchRef = useRef<() => void>(() => {})
|
|
||||||
const activeLogRefetchRef = useRef<() => void>(() => {})
|
|
||||||
const logsQueryRef = useRef({ isFetching: false, hasNextPage: false, fetchNextPage: () => {} })
|
|
||||||
const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false)
|
const [isNotificationSettingsOpen, setIsNotificationSettingsOpen] = useState(false)
|
||||||
const userPermissions = useUserPermissionsContext()
|
const userPermissions = useUserPermissionsContext()
|
||||||
|
|
||||||
@@ -141,19 +94,8 @@ export default function Logs() {
|
|||||||
const [previewLogId, setPreviewLogId] = useState<string | null>(null)
|
const [previewLogId, setPreviewLogId] = useState<string | null>(null)
|
||||||
|
|
||||||
const activeLogId = isPreviewOpen ? previewLogId : selectedLogId
|
const activeLogId = isPreviewOpen ? previewLogId : selectedLogId
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const detailRefetchInterval = useCallback(
|
|
||||||
(query: { state: { data?: WorkflowLog } }) => {
|
|
||||||
if (!isLive) return false
|
|
||||||
const status = query.state.data?.status
|
|
||||||
return status === 'running' || status === 'pending' ? 3000 : false
|
|
||||||
},
|
|
||||||
[isLive]
|
|
||||||
)
|
|
||||||
|
|
||||||
const activeLogQuery = useLogDetail(activeLogId ?? undefined, {
|
const activeLogQuery = useLogDetail(activeLogId ?? undefined, {
|
||||||
refetchInterval: detailRefetchInterval,
|
refetchInterval: isLive ? 3000 : false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const logFilters = useMemo(
|
const logFilters = useMemo(
|
||||||
@@ -212,73 +154,42 @@ export default function Logs() {
|
|||||||
return { ...selectedLogFromList, ...activeLogQuery.data }
|
return { ...selectedLogFromList, ...activeLogQuery.data }
|
||||||
}, [selectedLogFromList, activeLogQuery.data, isPreviewOpen])
|
}, [selectedLogFromList, activeLogQuery.data, isPreviewOpen])
|
||||||
|
|
||||||
const handleLogHover = useCallback(
|
|
||||||
(log: WorkflowLog) => {
|
|
||||||
prefetchLogDetail(queryClient, log.id)
|
|
||||||
},
|
|
||||||
[queryClient]
|
|
||||||
)
|
|
||||||
|
|
||||||
useFolders(workspaceId)
|
useFolders(workspaceId)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
logsRef.current = logs
|
|
||||||
}, [logs])
|
|
||||||
useEffect(() => {
|
|
||||||
selectedLogIndexRef.current = selectedLogIndex
|
|
||||||
}, [selectedLogIndex])
|
|
||||||
useEffect(() => {
|
|
||||||
selectedLogIdRef.current = selectedLogId
|
|
||||||
}, [selectedLogId])
|
|
||||||
useEffect(() => {
|
|
||||||
logsRefetchRef.current = logsQuery.refetch
|
|
||||||
}, [logsQuery.refetch])
|
|
||||||
useEffect(() => {
|
|
||||||
activeLogRefetchRef.current = activeLogQuery.refetch
|
|
||||||
}, [activeLogQuery.refetch])
|
|
||||||
useEffect(() => {
|
|
||||||
logsQueryRef.current = {
|
|
||||||
isFetching: logsQuery.isFetching,
|
|
||||||
hasNextPage: logsQuery.hasNextPage ?? false,
|
|
||||||
fetchNextPage: logsQuery.fetchNextPage,
|
|
||||||
}
|
|
||||||
}, [logsQuery.isFetching, logsQuery.hasNextPage, logsQuery.fetchNextPage])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timers = refreshTimersRef.current
|
|
||||||
return () => {
|
|
||||||
timers.forEach((id) => window.clearTimeout(id))
|
|
||||||
timers.clear()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialized.current) {
|
if (isInitialized.current) {
|
||||||
setStoreSearchQuery(debouncedSearchQuery)
|
setStoreSearchQuery(debouncedSearchQuery)
|
||||||
}
|
}
|
||||||
}, [debouncedSearchQuery, setStoreSearchQuery])
|
}, [debouncedSearchQuery, setStoreSearchQuery])
|
||||||
|
|
||||||
const handleLogClick = useCallback((log: WorkflowLog) => {
|
const handleLogClick = useCallback(
|
||||||
dispatch({ type: 'TOGGLE_LOG', logId: log.id })
|
(log: WorkflowLog) => {
|
||||||
}, [])
|
if (selectedLogId === log.id && isSidebarOpen) {
|
||||||
|
setIsSidebarOpen(false)
|
||||||
|
setSelectedLogId(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectedLogId(log.id)
|
||||||
|
setIsSidebarOpen(true)
|
||||||
|
},
|
||||||
|
[selectedLogId, isSidebarOpen]
|
||||||
|
)
|
||||||
|
|
||||||
const handleNavigateNext = useCallback(() => {
|
const handleNavigateNext = useCallback(() => {
|
||||||
const idx = selectedLogIndexRef.current
|
if (selectedLogIndex < logs.length - 1) {
|
||||||
const currentLogs = logsRef.current
|
setSelectedLogId(logs[selectedLogIndex + 1].id)
|
||||||
if (idx < currentLogs.length - 1) {
|
|
||||||
dispatch({ type: 'SELECT_LOG', logId: currentLogs[idx + 1].id })
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [selectedLogIndex, logs])
|
||||||
|
|
||||||
const handleNavigatePrev = useCallback(() => {
|
const handleNavigatePrev = useCallback(() => {
|
||||||
const idx = selectedLogIndexRef.current
|
if (selectedLogIndex > 0) {
|
||||||
if (idx > 0) {
|
setSelectedLogId(logs[selectedLogIndex - 1].id)
|
||||||
dispatch({ type: 'SELECT_LOG', logId: logsRef.current[idx - 1].id })
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [selectedLogIndex, logs])
|
||||||
|
|
||||||
const handleCloseSidebar = useCallback(() => {
|
const handleCloseSidebar = useCallback(() => {
|
||||||
dispatch({ type: 'CLOSE_SIDEBAR' })
|
setIsSidebarOpen(false)
|
||||||
|
setSelectedLogId(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLogContextMenu = useCallback((e: React.MouseEvent, log: WorkflowLog) => {
|
const handleLogContextMenu = useCallback((e: React.MouseEvent, log: WorkflowLog) => {
|
||||||
@@ -349,34 +260,26 @@ export default function Logs() {
|
|||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setIsVisuallyRefreshing(true)
|
setIsVisuallyRefreshing(true)
|
||||||
const timerId = window.setTimeout(() => {
|
setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS)
|
||||||
setIsVisuallyRefreshing(false)
|
logsQuery.refetch()
|
||||||
refreshTimersRef.current.delete(timerId)
|
if (selectedLogId) {
|
||||||
}, REFRESH_SPINNER_DURATION_MS)
|
activeLogQuery.refetch()
|
||||||
refreshTimersRef.current.add(timerId)
|
|
||||||
logsRefetchRef.current()
|
|
||||||
if (selectedLogIdRef.current) {
|
|
||||||
activeLogRefetchRef.current()
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [logsQuery, activeLogQuery, selectedLogId])
|
||||||
|
|
||||||
const handleToggleLive = useCallback(() => {
|
const handleToggleLive = useCallback(() => {
|
||||||
setIsLive((prev) => {
|
const newIsLive = !isLive
|
||||||
if (!prev) {
|
setIsLive(newIsLive)
|
||||||
|
|
||||||
|
if (newIsLive) {
|
||||||
setIsVisuallyRefreshing(true)
|
setIsVisuallyRefreshing(true)
|
||||||
const timerId = window.setTimeout(() => {
|
setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS)
|
||||||
setIsVisuallyRefreshing(false)
|
logsQuery.refetch()
|
||||||
refreshTimersRef.current.delete(timerId)
|
if (selectedLogId) {
|
||||||
}, REFRESH_SPINNER_DURATION_MS)
|
activeLogQuery.refetch()
|
||||||
refreshTimersRef.current.add(timerId)
|
|
||||||
logsRefetchRef.current()
|
|
||||||
if (selectedLogIdRef.current) {
|
|
||||||
activeLogRefetchRef.current()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return !prev
|
}, [isLive, logsQuery, activeLogQuery, selectedLogId])
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const prevIsFetchingRef = useRef(logsQuery.isFetching)
|
const prevIsFetchingRef = useRef(logsQuery.isFetching)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -386,15 +289,11 @@ export default function Logs() {
|
|||||||
|
|
||||||
if (isLive && !wasFetching && isFetching) {
|
if (isLive && !wasFetching && isFetching) {
|
||||||
setIsVisuallyRefreshing(true)
|
setIsVisuallyRefreshing(true)
|
||||||
const timerId = window.setTimeout(() => {
|
setTimeout(() => setIsVisuallyRefreshing(false), REFRESH_SPINNER_DURATION_MS)
|
||||||
setIsVisuallyRefreshing(false)
|
|
||||||
refreshTimersRef.current.delete(timerId)
|
|
||||||
}, REFRESH_SPINNER_DURATION_MS)
|
|
||||||
refreshTimersRef.current.add(timerId)
|
|
||||||
}
|
}
|
||||||
}, [logsQuery.isFetching, isLive])
|
}, [logsQuery.isFetching, isLive])
|
||||||
|
|
||||||
const handleExport = useCallback(async () => {
|
const handleExport = async () => {
|
||||||
setIsExporting(true)
|
setIsExporting(true)
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -428,17 +327,7 @@ export default function Logs() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false)
|
setIsExporting(false)
|
||||||
}
|
}
|
||||||
}, [
|
}
|
||||||
workspaceId,
|
|
||||||
level,
|
|
||||||
triggers,
|
|
||||||
workflowIds,
|
|
||||||
folderIds,
|
|
||||||
timeRange,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
debouncedSearchQuery,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialized.current) {
|
if (!isInitialized.current) {
|
||||||
@@ -459,59 +348,41 @@ export default function Logs() {
|
|||||||
}, [initializeFromURL])
|
}, [initializeFromURL])
|
||||||
|
|
||||||
const loadMoreLogs = useCallback(() => {
|
const loadMoreLogs = useCallback(() => {
|
||||||
const { isFetching, hasNextPage, fetchNextPage } = logsQueryRef.current
|
if (!logsQuery.isFetching && logsQuery.hasNextPage) {
|
||||||
if (!isFetching && hasNextPage) {
|
logsQuery.fetchNextPage()
|
||||||
fetchNextPage()
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [logsQuery])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (isSearchOpenRef.current) return
|
if (isSearchOpenRef.current) return
|
||||||
const currentLogs = logsRef.current
|
if (logs.length === 0) return
|
||||||
const currentIndex = selectedLogIndexRef.current
|
|
||||||
if (currentLogs.length === 0) return
|
|
||||||
|
|
||||||
if (currentIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
if (selectedLogIndex === -1 && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
dispatch({ type: 'SELECT_LOG', logId: currentLogs[0].id })
|
setSelectedLogId(logs[0].id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'ArrowUp' && !e.metaKey && !e.ctrlKey && currentIndex > 0) {
|
if (e.key === 'ArrowUp' && !e.metaKey && !e.ctrlKey && selectedLogIndex > 0) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleNavigatePrev()
|
handleNavigatePrev()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (e.key === 'ArrowDown' && !e.metaKey && !e.ctrlKey && selectedLogIndex < logs.length - 1) {
|
||||||
e.key === 'ArrowDown' &&
|
|
||||||
!e.metaKey &&
|
|
||||||
!e.ctrlKey &&
|
|
||||||
currentIndex < currentLogs.length - 1
|
|
||||||
) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleNavigateNext()
|
handleNavigateNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Enter' && selectedLogIdRef.current) {
|
if (e.key === 'Enter' && selectedLogId) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
dispatch({ type: 'TOGGLE_SIDEBAR' })
|
setIsSidebarOpen(!isSidebarOpen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
}, [handleNavigateNext, handleNavigatePrev])
|
}, [logs, selectedLogIndex, isSidebarOpen, selectedLogId, handleNavigateNext, handleNavigatePrev])
|
||||||
|
|
||||||
const handleCloseContextMenu = useCallback(() => setContextMenuOpen(false), [])
|
|
||||||
const handleOpenNotificationSettings = useCallback(() => setIsNotificationSettingsOpen(true), [])
|
|
||||||
const handleSearchOpenChange = useCallback((open: boolean) => {
|
|
||||||
isSearchOpenRef.current = open
|
|
||||||
}, [])
|
|
||||||
const handleClosePreview = useCallback(() => {
|
|
||||||
setIsPreviewOpen(false)
|
|
||||||
setPreviewLogId(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const isDashboardView = viewMode === 'dashboard'
|
const isDashboardView = viewMode === 'dashboard'
|
||||||
|
|
||||||
@@ -531,10 +402,12 @@ export default function Logs() {
|
|||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
canEdit={userPermissions.canEdit}
|
canEdit={userPermissions.canEdit}
|
||||||
hasLogs={logs.length > 0}
|
hasLogs={logs.length > 0}
|
||||||
onOpenNotificationSettings={handleOpenNotificationSettings}
|
onOpenNotificationSettings={() => setIsNotificationSettingsOpen(true)}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchQueryChange={setSearchQuery}
|
onSearchQueryChange={setSearchQuery}
|
||||||
onSearchOpenChange={handleSearchOpenChange}
|
onSearchOpenChange={(open: boolean) => {
|
||||||
|
isSearchOpenRef.current = open
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -576,7 +449,7 @@ export default function Logs() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table body - virtualized */}
|
{/* Table body - virtualized */}
|
||||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
<div className='min-h-0 flex-1 overflow-hidden' ref={scrollContainerRef}>
|
||||||
{logsQuery.isLoading && !logsQuery.data ? (
|
{logsQuery.isLoading && !logsQuery.data ? (
|
||||||
<div className='flex h-full items-center justify-center'>
|
<div className='flex h-full items-center justify-center'>
|
||||||
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
|
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
|
||||||
@@ -603,7 +476,6 @@ export default function Logs() {
|
|||||||
logs={logs}
|
logs={logs}
|
||||||
selectedLogId={selectedLogId}
|
selectedLogId={selectedLogId}
|
||||||
onLogClick={handleLogClick}
|
onLogClick={handleLogClick}
|
||||||
onLogHover={handleLogHover}
|
|
||||||
onLogContextMenu={handleLogContextMenu}
|
onLogContextMenu={handleLogContextMenu}
|
||||||
selectedRowRef={selectedRowRef}
|
selectedRowRef={selectedRowRef}
|
||||||
hasNextPage={logsQuery.hasNextPage ?? false}
|
hasNextPage={logsQuery.hasNextPage ?? false}
|
||||||
@@ -639,7 +511,7 @@ export default function Logs() {
|
|||||||
isOpen={contextMenuOpen}
|
isOpen={contextMenuOpen}
|
||||||
position={contextMenuPosition}
|
position={contextMenuPosition}
|
||||||
menuRef={contextMenuRef}
|
menuRef={contextMenuRef}
|
||||||
onClose={handleCloseContextMenu}
|
onClose={() => setContextMenuOpen(false)}
|
||||||
log={contextMenuLog}
|
log={contextMenuLog}
|
||||||
onCopyExecutionId={handleCopyExecutionId}
|
onCopyExecutionId={handleCopyExecutionId}
|
||||||
onOpenWorkflow={handleOpenWorkflow}
|
onOpenWorkflow={handleOpenWorkflow}
|
||||||
@@ -656,7 +528,10 @@ export default function Logs() {
|
|||||||
traceSpans={activeLogQuery.data.executionData?.traceSpans}
|
traceSpans={activeLogQuery.data.executionData?.traceSpans}
|
||||||
isModal
|
isModal
|
||||||
isOpen={isPreviewOpen}
|
isOpen={isPreviewOpen}
|
||||||
onClose={handleClosePreview}
|
onClose={() => {
|
||||||
|
setIsPreviewOpen(false)
|
||||||
|
setPreviewLogId(null)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -239,13 +239,8 @@ export const ComboBox = memo(function ComboBox({
|
|||||||
*/
|
*/
|
||||||
const defaultOptionValue = useMemo(() => {
|
const defaultOptionValue = useMemo(() => {
|
||||||
if (defaultValue !== undefined) {
|
if (defaultValue !== undefined) {
|
||||||
// Validate that the default value exists in the available (filtered) options
|
|
||||||
const defaultInOptions = evaluatedOptions.find((opt) => getOptionValue(opt) === defaultValue)
|
|
||||||
if (defaultInOptions) {
|
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
// Default not available (e.g. provider disabled) — fall through to other fallbacks
|
|
||||||
}
|
|
||||||
|
|
||||||
// For model field, default to claude-sonnet-4-5 if available
|
// For model field, default to claude-sonnet-4-5 if available
|
||||||
if (subBlockId === 'model') {
|
if (subBlockId === 'model') {
|
||||||
|
|||||||
@@ -223,12 +223,7 @@ function resolveToolsDisplay(
|
|||||||
* - Resolves tool names from block registry
|
* - Resolves tool names from block registry
|
||||||
* - Shows '-' for other selector types that need hydration
|
* - Shows '-' for other selector types that need hydration
|
||||||
*/
|
*/
|
||||||
const SubBlockRow = memo(function SubBlockRow({
|
function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) {
|
||||||
title,
|
|
||||||
value,
|
|
||||||
subBlock,
|
|
||||||
rawValue,
|
|
||||||
}: SubBlockRowProps) {
|
|
||||||
const isPasswordField = subBlock?.password === true
|
const isPasswordField = subBlock?.password === true
|
||||||
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
|
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
|
||||||
|
|
||||||
@@ -260,7 +255,7 @@ const SubBlockRow = memo(function SubBlockRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preview block component for workflow visualization.
|
* Preview block component for workflow visualization.
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { AgentIcon } from '@/components/icons'
|
import { AgentIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { AuthMode } from '@/blocks/types'
|
import { AuthMode } from '@/blocks/types'
|
||||||
import { getApiKeyCondition, getModelOptions } from '@/blocks/utils'
|
import { getApiKeyCondition } from '@/blocks/utils'
|
||||||
import {
|
import {
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
getMaxTemperature,
|
getMaxTemperature,
|
||||||
|
getProviderIcon,
|
||||||
getReasoningEffortValuesForModel,
|
getReasoningEffortValuesForModel,
|
||||||
getThinkingLevelsForModel,
|
getThinkingLevelsForModel,
|
||||||
getVerbosityValuesForModel,
|
getVerbosityValuesForModel,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
providers,
|
providers,
|
||||||
supportsTemperature,
|
supportsTemperature,
|
||||||
} from '@/providers/utils'
|
} from '@/providers/utils'
|
||||||
|
import { useProvidersStore } from '@/stores/providers'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('AgentBlock')
|
const logger = createLogger('AgentBlock')
|
||||||
@@ -119,7 +121,21 @@ Return ONLY the JSON array.`,
|
|||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: 'claude-sonnet-4-5',
|
defaultValue: 'claude-sonnet-4-5',
|
||||||
options: getModelOptions,
|
options: () => {
|
||||||
|
const providersState = useProvidersStore.getState()
|
||||||
|
const baseModels = providersState.providers.base.models
|
||||||
|
const ollamaModels = providersState.providers.ollama.models
|
||||||
|
const vllmModels = providersState.providers.vllm.models
|
||||||
|
const openrouterModels = providersState.providers.openrouter.models
|
||||||
|
const allModels = Array.from(
|
||||||
|
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
||||||
|
)
|
||||||
|
|
||||||
|
return allModels.map((model) => {
|
||||||
|
const icon = getProviderIcon(model)
|
||||||
|
return { label: model, id: model, ...(icon && { icon }) }
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'vertexCredential',
|
id: 'vertexCredential',
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { ChartBarIcon } from '@/components/icons'
|
import { ChartBarIcon } from '@/components/icons'
|
||||||
import type { BlockConfig, ParamType } from '@/blocks/types'
|
import type { BlockConfig, ParamType } from '@/blocks/types'
|
||||||
import {
|
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
||||||
getModelOptions,
|
|
||||||
getProviderCredentialSubBlocks,
|
|
||||||
PROVIDER_CREDENTIAL_INPUTS,
|
|
||||||
} from '@/blocks/utils'
|
|
||||||
import type { ProviderId } from '@/providers/types'
|
import type { ProviderId } from '@/providers/types'
|
||||||
import { getBaseModelProviders } from '@/providers/utils'
|
import { getBaseModelProviders, getProviderIcon } from '@/providers/utils'
|
||||||
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('EvaluatorBlock')
|
const logger = createLogger('EvaluatorBlock')
|
||||||
@@ -178,7 +175,21 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
|
|||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: 'claude-sonnet-4-5',
|
defaultValue: 'claude-sonnet-4-5',
|
||||||
options: getModelOptions,
|
options: () => {
|
||||||
|
const providersState = useProvidersStore.getState()
|
||||||
|
const baseModels = providersState.providers.base.models
|
||||||
|
const ollamaModels = providersState.providers.ollama.models
|
||||||
|
const vllmModels = providersState.providers.vllm.models
|
||||||
|
const openrouterModels = providersState.providers.openrouter.models
|
||||||
|
const allModels = Array.from(
|
||||||
|
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
||||||
|
)
|
||||||
|
|
||||||
|
return allModels.map((model) => {
|
||||||
|
const icon = getProviderIcon(model)
|
||||||
|
return { label: model, id: model, ...(icon && { icon }) }
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...getProviderCredentialSubBlocks(),
|
...getProviderCredentialSubBlocks(),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ShieldCheckIcon } from '@/components/icons'
|
import { ShieldCheckIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import {
|
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
||||||
getModelOptions,
|
import { getProviderIcon } from '@/providers/utils'
|
||||||
getProviderCredentialSubBlocks,
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
PROVIDER_CREDENTIAL_INPUTS,
|
|
||||||
} from '@/blocks/utils'
|
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
export interface GuardrailsResponse extends ToolResponse {
|
export interface GuardrailsResponse extends ToolResponse {
|
||||||
@@ -113,7 +111,21 @@ Return ONLY the regex pattern - no explanations, no quotes, no forward slashes,
|
|||||||
type: 'combobox',
|
type: 'combobox',
|
||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
options: getModelOptions,
|
options: () => {
|
||||||
|
const providersState = useProvidersStore.getState()
|
||||||
|
const baseModels = providersState.providers.base.models
|
||||||
|
const ollamaModels = providersState.providers.ollama.models
|
||||||
|
const vllmModels = providersState.providers.vllm.models
|
||||||
|
const openrouterModels = providersState.providers.openrouter.models
|
||||||
|
const allModels = Array.from(
|
||||||
|
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
||||||
|
)
|
||||||
|
|
||||||
|
return allModels.map((model) => {
|
||||||
|
const icon = getProviderIcon(model)
|
||||||
|
return { label: model, id: model, ...(icon && { icon }) }
|
||||||
|
})
|
||||||
|
},
|
||||||
condition: {
|
condition: {
|
||||||
field: 'validationType',
|
field: 'validationType',
|
||||||
value: ['hallucination'],
|
value: ['hallucination'],
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { ConnectIcon } from '@/components/icons'
|
import { ConnectIcon } from '@/components/icons'
|
||||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import {
|
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
||||||
getModelOptions,
|
|
||||||
getProviderCredentialSubBlocks,
|
|
||||||
PROVIDER_CREDENTIAL_INPUTS,
|
|
||||||
} from '@/blocks/utils'
|
|
||||||
import type { ProviderId } from '@/providers/types'
|
import type { ProviderId } from '@/providers/types'
|
||||||
import { getBaseModelProviders } from '@/providers/utils'
|
import { getBaseModelProviders, getProviderIcon } from '@/providers/utils'
|
||||||
|
import { useProvidersStore } from '@/stores/providers'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
interface RouterResponse extends ToolResponse {
|
interface RouterResponse extends ToolResponse {
|
||||||
@@ -137,6 +134,25 @@ Respond with a JSON object containing:
|
|||||||
- reasoning: A brief explanation (1-2 sentences) of why you chose this route`
|
- reasoning: A brief explanation (1-2 sentences) of why you chose this route`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get model options for both router versions.
|
||||||
|
*/
|
||||||
|
const getModelOptions = () => {
|
||||||
|
const providersState = useProvidersStore.getState()
|
||||||
|
const baseModels = providersState.providers.base.models
|
||||||
|
const ollamaModels = providersState.providers.ollama.models
|
||||||
|
const vllmModels = providersState.providers.vllm.models
|
||||||
|
const openrouterModels = providersState.providers.openrouter.models
|
||||||
|
const allModels = Array.from(
|
||||||
|
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
||||||
|
)
|
||||||
|
|
||||||
|
return allModels.map((model) => {
|
||||||
|
const icon = getProviderIcon(model)
|
||||||
|
return { label: model, id: model, ...(icon && { icon }) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy Router Block (block-based routing).
|
* Legacy Router Block (block-based routing).
|
||||||
* Hidden from toolbar but still supported for existing workflows.
|
* Hidden from toolbar but still supported for existing workflows.
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { TranslateIcon } from '@/components/icons'
|
import { TranslateIcon } from '@/components/icons'
|
||||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import {
|
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
||||||
getModelOptions,
|
import { getProviderIcon } from '@/providers/utils'
|
||||||
getProviderCredentialSubBlocks,
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
PROVIDER_CREDENTIAL_INPUTS,
|
|
||||||
} from '@/blocks/utils'
|
|
||||||
|
|
||||||
const getTranslationPrompt = (targetLanguage: string) =>
|
const getTranslationPrompt = (targetLanguage: string) =>
|
||||||
`Translate the following text into ${targetLanguage || 'English'}. Output ONLY the translated text with no additional commentary, explanations, or notes.`
|
`Translate the following text into ${targetLanguage || 'English'}. Output ONLY the translated text with no additional commentary, explanations, or notes.`
|
||||||
@@ -40,7 +38,18 @@ export const TranslateBlock: BlockConfig = {
|
|||||||
type: 'combobox',
|
type: 'combobox',
|
||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
options: getModelOptions,
|
options: () => {
|
||||||
|
const providersState = useProvidersStore.getState()
|
||||||
|
const baseModels = providersState.providers.base.models
|
||||||
|
const ollamaModels = providersState.providers.ollama.models
|
||||||
|
const openrouterModels = providersState.providers.openrouter.models
|
||||||
|
const allModels = Array.from(new Set([...baseModels, ...ollamaModels, ...openrouterModels]))
|
||||||
|
|
||||||
|
return allModels.map((model) => {
|
||||||
|
const icon = getProviderIcon(model)
|
||||||
|
return { label: model, id: model, ...(icon && { icon }) }
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...getProviderCredentialSubBlocks(),
|
...getProviderCredentialSubBlocks(),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,32 +1,8 @@
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
||||||
import {
|
import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils'
|
||||||
getHostedModels,
|
|
||||||
getProviderFromModel,
|
|
||||||
getProviderIcon,
|
|
||||||
providers,
|
|
||||||
} from '@/providers/utils'
|
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns model options for combobox subblocks, combining all provider sources.
|
|
||||||
*/
|
|
||||||
export function getModelOptions() {
|
|
||||||
const providersState = useProvidersStore.getState()
|
|
||||||
const baseModels = providersState.providers.base.models
|
|
||||||
const ollamaModels = providersState.providers.ollama.models
|
|
||||||
const vllmModels = providersState.providers.vllm.models
|
|
||||||
const openrouterModels = providersState.providers.openrouter.models
|
|
||||||
const allModels = Array.from(
|
|
||||||
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
|
||||||
)
|
|
||||||
|
|
||||||
return allModels.map((model) => {
|
|
||||||
const icon = getProviderIcon(model)
|
|
||||||
return { label: model, id: model, ...(icon && { icon }) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a field is included in the dependsOn config.
|
* Checks if a field is included in the dependsOn config.
|
||||||
* Handles both simple array format and object format with all/any fields.
|
* Handles both simple array format and object format with all/any fields.
|
||||||
|
|||||||
@@ -1008,7 +1008,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
|
|||||||
* Non-virtualized code viewer implementation.
|
* Non-virtualized code viewer implementation.
|
||||||
* Renders all lines directly without windowing.
|
* Renders all lines directly without windowing.
|
||||||
*/
|
*/
|
||||||
const ViewerInner = memo(function ViewerInner({
|
function ViewerInner({
|
||||||
code,
|
code,
|
||||||
showGutter,
|
showGutter,
|
||||||
language,
|
language,
|
||||||
@@ -1181,7 +1181,7 @@ const ViewerInner = memo(function ViewerInner({
|
|||||||
</Content>
|
</Content>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Readonly code viewer with optional gutter and syntax highlighting.
|
* Readonly code viewer with optional gutter and syntax highlighting.
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||||
keepPreviousData,
|
|
||||||
type QueryClient,
|
|
||||||
useInfiniteQuery,
|
|
||||||
useQuery,
|
|
||||||
useQueryClient,
|
|
||||||
} from '@tanstack/react-query'
|
|
||||||
import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters'
|
import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters'
|
||||||
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser'
|
||||||
import type {
|
import type {
|
||||||
@@ -152,45 +146,17 @@ export function useLogsList(
|
|||||||
|
|
||||||
interface UseLogDetailOptions {
|
interface UseLogDetailOptions {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
refetchInterval?:
|
refetchInterval?: number | false
|
||||||
| number
|
|
||||||
| false
|
|
||||||
| ((query: { state: { data?: WorkflowLog } }) => number | false | undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLogDetail(logId: string | undefined, options?: UseLogDetailOptions) {
|
export function useLogDetail(logId: string | undefined, options?: UseLogDetailOptions) {
|
||||||
const queryClient = useQueryClient()
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: logKeys.detail(logId),
|
queryKey: logKeys.detail(logId),
|
||||||
queryFn: () => fetchLogDetail(logId as string),
|
queryFn: () => fetchLogDetail(logId as string),
|
||||||
enabled: Boolean(logId) && (options?.enabled ?? true),
|
enabled: Boolean(logId) && (options?.enabled ?? true),
|
||||||
refetchInterval: options?.refetchInterval ?? false,
|
refetchInterval: options?.refetchInterval ?? false,
|
||||||
staleTime: 30 * 1000,
|
staleTime: 30 * 1000,
|
||||||
initialData: () => {
|
placeholderData: keepPreviousData,
|
||||||
if (!logId) return undefined
|
|
||||||
const listQueries = queryClient.getQueriesData<{
|
|
||||||
pages: { logs: WorkflowLog[] }[]
|
|
||||||
}>({
|
|
||||||
queryKey: logKeys.lists(),
|
|
||||||
})
|
|
||||||
for (const [, data] of listQueries) {
|
|
||||||
const match = data?.pages?.flatMap((p) => p.logs).find((l) => l.id === logId)
|
|
||||||
if (match) return match
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
initialDataUpdatedAt: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefetches log detail data on hover for instant panel rendering on click.
|
|
||||||
*/
|
|
||||||
export function prefetchLogDetail(queryClient: QueryClient, logId: string) {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: logKeys.detail(logId),
|
|
||||||
queryFn: () => fetchLogDetail(logId),
|
|
||||||
staleTime: 30 * 1000,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export const vertexProvider: ProviderConfig = {
|
|||||||
executeRequest: async (
|
executeRequest: async (
|
||||||
request: ProviderRequest
|
request: ProviderRequest
|
||||||
): Promise<ProviderResponse | StreamingExecution> => {
|
): Promise<ProviderResponse | StreamingExecution> => {
|
||||||
const vertexProject = request.vertexProject || env.VERTEX_PROJECT
|
const vertexProject = env.VERTEX_PROJECT || request.vertexProject
|
||||||
const vertexLocation = request.vertexLocation || env.VERTEX_LOCATION || 'us-central1'
|
const vertexLocation = env.VERTEX_LOCATION || request.vertexLocation || 'us-central1'
|
||||||
|
|
||||||
if (!vertexProject) {
|
if (!vertexProject) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
Reference in New Issue
Block a user