Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { StatusBarSegment } from './status-bar'
export { default, StatusBar } from './status-bar'
export { StatusBar } from './status-bar'
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface StatusBarSegment {
timestamp: string
}

export function StatusBar({
function StatusBarInner({
segments,
selectedSegmentIndices,
onSegmentClick,
Expand Down Expand Up @@ -127,4 +127,45 @@ export function StatusBar({
)
}

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)
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { WorkflowExecutionItem } from './workflows-list'
export { default, WorkflowsList } from './workflows-list'
export { WorkflowsList } from './workflows-list'
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface WorkflowExecutionItem {
overallSuccessRate: number
}

export function WorkflowsList({
function WorkflowsListInner({
filteredExecutions,
expandedWorkflowId,
onToggleWorkflow,
Expand Down Expand Up @@ -103,7 +103,7 @@ export function WorkflowsList({
<StatusBar
segments={workflow.segments}
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
onSegmentClick={onSegmentClick as any}
onSegmentClick={onSegmentClick}
workflowId={workflow.workflowId}
segmentDurationMs={segmentDurationMs}
preferBelow={idx < 2}
Expand All @@ -124,4 +124,4 @@ export function WorkflowsList({
)
}

export default memo(WorkflowsList)
export const WorkflowsList = memo(WorkflowsListInner)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Skeleton } from '@/components/ui/skeleton'
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
Expand Down Expand Up @@ -141,31 +141,90 @@ function toWorkflowExecution(wf: WorkflowStats): WorkflowExecution {
}
}

export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
function DashboardInner({ stats, isLoading, error }: DashboardProps) {
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
const barsAreaRef = useRef<HTMLDivElement | null>(null)
const lastAnchorIndicesRef = useRef<Record<string, number>>({})

const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore()

const allWorkflows = useWorkflowRegistry((state) => state.workflows)

const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null

const { executions, aggregateSegments, segmentMs } = useMemo(() => {
const { rawExecutions, aggregateSegments, segmentMs } = useMemo(() => {
if (!stats) {
return { executions: [], aggregateSegments: [], segmentMs: 0 }
return { rawExecutions: [], aggregateSegments: [], segmentMs: 0 }
}

const workflowExecutions = stats.workflows.map(toWorkflowExecution)

return {
executions: workflowExecutions,
rawExecutions: stats.workflows.map(toWorkflowExecution),
aggregateSegments: stats.aggregateSegments,
segmentMs: stats.segmentMs,
}
}, [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 map = new Map<string, number>()
for (const wf of executions) {
Expand Down Expand Up @@ -312,6 +371,8 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
[toggleWorkflowId]
)

lastAnchorIndicesRef.current = lastAnchorIndices

/**
* Handles segment click for selecting time segments.
* @param workflowId - The workflow containing the segment
Expand Down Expand Up @@ -361,7 +422,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
} else if (mode === 'range') {
setSelectedSegments((prev) => {
const currentSegments = prev[workflowId] || []
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
const anchor = lastAnchorIndicesRef.current[workflowId] ?? segmentIndex
const [start, end] =
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
Expand All @@ -370,12 +431,12 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
})
}
},
[lastAnchorIndices]
[]
)

useEffect(() => {
setSelectedSegments({})
setLastAnchorIndices({})
setSelectedSegments((prev) => (Object.keys(prev).length > 0 ? {} : prev))
setLastAnchorIndices((prev) => (Object.keys(prev).length > 0 ? {} : prev))
}, [stats, timeRange, workflowIds, searchQuery])

if (isLoading) {
Expand Down Expand Up @@ -493,7 +554,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
</div>
</div>

<div className='min-h-0 flex-1 overflow-hidden' ref={barsAreaRef}>
<div className='min-h-0 flex-1 overflow-hidden'>
<WorkflowsList
filteredExecutions={filteredExecutions as WorkflowExecution[]}
expandedWorkflowId={expandedWorkflowId}
Expand All @@ -507,3 +568,5 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
</div>
)
}

export default memo(DashboardInner)
Loading