Skip to content

Commit ab3a3d1

Browse files
improvement(logs): dashboard/logs optimizations and improvements (#2414)
* improvement(logs): dashboard/logs optimizations and improvements * improvement: addressed comments * improvement: loading * cleanup * ack PR comments * cleanup more --------- Co-authored-by: waleed <[email protected]>
1 parent e01d4cb commit ab3a3d1

File tree

27 files changed

+1342
-1384
lines changed

27 files changed

+1342
-1384
lines changed

apps/sim/app/api/workspaces/[id]/metrics/executions/route.ts

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const QueryParamsSchema = z.object({
1616
folderIds: z.string().optional(),
1717
triggers: z.string().optional(),
1818
level: z.string().optional(), // Supports comma-separated values: 'error,running'
19+
allTime: z
20+
.enum(['true', 'false'])
21+
.optional()
22+
.transform((v) => v === 'true'),
1923
})
2024

2125
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -29,17 +33,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
2933
}
3034
const userId = session.user.id
3135

32-
const end = qp.endTime ? new Date(qp.endTime) : new Date()
33-
const start = qp.startTime
36+
let end = qp.endTime ? new Date(qp.endTime) : new Date()
37+
let start = qp.startTime
3438
? new Date(qp.startTime)
3539
: new Date(end.getTime() - 24 * 60 * 60 * 1000)
36-
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || start >= end) {
40+
41+
const isAllTime = qp.allTime === true
42+
43+
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
3744
return NextResponse.json({ error: 'Invalid time range' }, { status: 400 })
3845
}
3946

4047
const segments = qp.segments
41-
const totalMs = Math.max(1, end.getTime() - start.getTime())
42-
const segmentMs = Math.max(1, Math.floor(totalMs / Math.max(1, segments)))
4348

4449
const [permission] = await db
4550
.select()
@@ -75,23 +80,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
7580
workflows: [],
7681
startTime: start.toISOString(),
7782
endTime: end.toISOString(),
78-
segmentMs,
83+
segmentMs: 0,
7984
})
8085
}
8186

8287
const workflowIdList = workflows.map((w) => w.id)
8388

84-
const logWhere = [
85-
inArray(workflowExecutionLogs.workflowId, workflowIdList),
86-
gte(workflowExecutionLogs.startedAt, start),
87-
lte(workflowExecutionLogs.startedAt, end),
88-
] as SQL[]
89+
const baseLogWhere = [inArray(workflowExecutionLogs.workflowId, workflowIdList)] as SQL[]
8990
if (qp.triggers) {
9091
const t = qp.triggers.split(',').filter(Boolean)
91-
logWhere.push(inArray(workflowExecutionLogs.trigger, t))
92+
baseLogWhere.push(inArray(workflowExecutionLogs.trigger, t))
9293
}
9394

94-
// Handle level filtering with support for derived statuses and multiple selections
9595
if (qp.level && qp.level !== 'all') {
9696
const levels = qp.level.split(',').filter(Boolean)
9797
const levelConditions: SQL[] = []
@@ -100,21 +100,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
100100
if (level === 'error') {
101101
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
102102
} else if (level === 'info') {
103-
// Completed info logs only
104103
const condition = and(
105104
eq(workflowExecutionLogs.level, 'info'),
106105
isNotNull(workflowExecutionLogs.endedAt)
107106
)
108107
if (condition) levelConditions.push(condition)
109108
} else if (level === 'running') {
110-
// Running logs: info level with no endedAt
111109
const condition = and(
112110
eq(workflowExecutionLogs.level, 'info'),
113111
isNull(workflowExecutionLogs.endedAt)
114112
)
115113
if (condition) levelConditions.push(condition)
116114
} else if (level === 'pending') {
117-
// Pending logs: info level with pause status indicators
118115
const condition = and(
119116
eq(workflowExecutionLogs.level, 'info'),
120117
or(
@@ -132,10 +129,55 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
132129
if (levelConditions.length > 0) {
133130
const combinedCondition =
134131
levelConditions.length === 1 ? levelConditions[0] : or(...levelConditions)
135-
if (combinedCondition) logWhere.push(combinedCondition)
132+
if (combinedCondition) baseLogWhere.push(combinedCondition)
136133
}
137134
}
138135

136+
if (isAllTime) {
137+
const boundsQuery = db
138+
.select({
139+
minDate: sql<Date>`MIN(${workflowExecutionLogs.startedAt})`,
140+
maxDate: sql<Date>`MAX(${workflowExecutionLogs.startedAt})`,
141+
})
142+
.from(workflowExecutionLogs)
143+
.leftJoin(
144+
pausedExecutions,
145+
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
146+
)
147+
.where(and(...baseLogWhere))
148+
149+
const [bounds] = await boundsQuery
150+
151+
if (bounds?.minDate && bounds?.maxDate) {
152+
start = new Date(bounds.minDate)
153+
end = new Date(Math.max(new Date(bounds.maxDate).getTime(), Date.now()))
154+
} else {
155+
return NextResponse.json({
156+
workflows: workflows.map((wf) => ({
157+
workflowId: wf.id,
158+
workflowName: wf.name,
159+
segments: [],
160+
})),
161+
startTime: new Date().toISOString(),
162+
endTime: new Date().toISOString(),
163+
segmentMs: 0,
164+
})
165+
}
166+
}
167+
168+
if (start >= end) {
169+
return NextResponse.json({ error: 'Invalid time range' }, { status: 400 })
170+
}
171+
172+
const totalMs = Math.max(1, end.getTime() - start.getTime())
173+
const segmentMs = Math.max(1, Math.floor(totalMs / Math.max(1, segments)))
174+
175+
const logWhere = [
176+
...baseLogWhere,
177+
gte(workflowExecutionLogs.startedAt, start),
178+
lte(workflowExecutionLogs.startedAt, end),
179+
]
180+
139181
const logs = await db
140182
.select({
141183
workflowId: workflowExecutionLogs.workflowId,
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export type { LineChartMultiSeries, LineChartPoint } from './line-chart'
2-
export { default, LineChart } from './line-chart'
1+
export { default, LineChart, type LineChartMultiSeries, type LineChartPoint } from './line-chart'

apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx

Lines changed: 110 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from 'react'
1+
import { memo, useEffect, useMemo, useRef, useState } from 'react'
22
import { cn } from '@/lib/core/utils/cn'
33
import { formatDate, formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
44

@@ -15,7 +15,7 @@ export interface LineChartMultiSeries {
1515
dashed?: boolean
1616
}
1717

18-
export function LineChart({
18+
function LineChartComponent({
1919
data,
2020
label,
2121
color,
@@ -95,92 +95,92 @@ export function LineChart({
9595

9696
const hasExternalWrapper = !label || label === ''
9797

98-
if (containerWidth === null) {
99-
return (
100-
<div
101-
ref={containerRef}
102-
className={cn('w-full', !hasExternalWrapper && 'rounded-lg border bg-card p-4')}
103-
style={{ height }}
104-
/>
105-
)
106-
}
107-
108-
if (data.length === 0) {
109-
return (
110-
<div
111-
className={cn(
112-
'flex items-center justify-center',
113-
!hasExternalWrapper && 'rounded-lg border bg-card p-4'
114-
)}
115-
style={{ width, height }}
116-
>
117-
<p className='text-muted-foreground text-sm'>No data</p>
118-
</div>
119-
)
120-
}
98+
const allSeries = useMemo(
99+
() =>
100+
(Array.isArray(series) && series.length > 0
101+
? [{ id: 'base', label, color, data }, ...series]
102+
: [{ id: 'base', label, color, data }]
103+
).map((s, idx) => ({ ...s, id: s.id || s.label || String(idx) })),
104+
[series, label, color, data]
105+
)
121106

122-
const allSeries = (
123-
Array.isArray(series) && series.length > 0
124-
? [{ id: 'base', label, color, data }, ...series]
125-
: [{ id: 'base', label, color, data }]
126-
).map((s, idx) => ({ ...s, id: s.id || s.label || String(idx) }))
127-
128-
const flatValues = allSeries.flatMap((s) => s.data.map((d) => d.value))
129-
const rawMax = Math.max(...flatValues, 1)
130-
const rawMin = Math.min(...flatValues, 0)
131-
const paddedMax = rawMax === 0 ? 1 : rawMax * 1.1
132-
const paddedMin = Math.min(0, rawMin)
133-
const unitSuffixPre = (unit || '').trim().toLowerCase()
134-
let maxValue = Math.ceil(paddedMax)
135-
let minValue = Math.floor(paddedMin)
136-
if (unitSuffixPre === 'ms' || unitSuffixPre === 'latency') {
137-
minValue = 0
138-
if (paddedMax < 10) {
139-
maxValue = Math.ceil(paddedMax)
140-
} else if (paddedMax < 100) {
141-
maxValue = Math.ceil(paddedMax / 10) * 10
142-
} else if (paddedMax < 1000) {
143-
maxValue = Math.ceil(paddedMax / 50) * 50
144-
} else if (paddedMax < 10000) {
145-
maxValue = Math.ceil(paddedMax / 500) * 500
146-
} else {
147-
maxValue = Math.ceil(paddedMax / 1000) * 1000
107+
const { maxValue, minValue, valueRange } = useMemo(() => {
108+
const flatValues = allSeries.flatMap((s) => s.data.map((d) => d.value))
109+
const rawMax = Math.max(...flatValues, 1)
110+
const rawMin = Math.min(...flatValues, 0)
111+
const paddedMax = rawMax === 0 ? 1 : rawMax * 1.1
112+
const paddedMin = Math.min(0, rawMin)
113+
const unitSuffixPre = (unit || '').trim().toLowerCase()
114+
let maxVal = Math.ceil(paddedMax)
115+
let minVal = Math.floor(paddedMin)
116+
if (unitSuffixPre === 'ms' || unitSuffixPre === 'latency') {
117+
minVal = 0
118+
if (paddedMax < 10) {
119+
maxVal = Math.ceil(paddedMax)
120+
} else if (paddedMax < 100) {
121+
maxVal = Math.ceil(paddedMax / 10) * 10
122+
} else if (paddedMax < 1000) {
123+
maxVal = Math.ceil(paddedMax / 50) * 50
124+
} else if (paddedMax < 10000) {
125+
maxVal = Math.ceil(paddedMax / 500) * 500
126+
} else {
127+
maxVal = Math.ceil(paddedMax / 1000) * 1000
128+
}
148129
}
149-
}
150-
const valueRange = maxValue - minValue || 1
130+
return {
131+
maxValue: maxVal,
132+
minValue: minVal,
133+
valueRange: maxVal - minVal || 1,
134+
}
135+
}, [allSeries, unit])
151136

152137
const yMin = padding.top + 3
153138
const yMax = padding.top + chartHeight - 3
154139

155-
const scaledPoints = data.map((d, i) => {
156-
const usableW = Math.max(1, chartWidth)
157-
const x = padding.left + (i / (data.length - 1 || 1)) * usableW
158-
const rawY = padding.top + chartHeight - ((d.value - minValue) / valueRange) * chartHeight
159-
const y = Math.max(yMin, Math.min(yMax, rawY))
160-
return { x, y }
161-
})
162-
163-
const scaledSeries = allSeries.map((s) => {
164-
const pts = s.data.map((d, i) => {
165-
const usableW = Math.max(1, chartWidth)
166-
const x = padding.left + (i / (s.data.length - 1 || 1)) * usableW
167-
const rawY = padding.top + chartHeight - ((d.value - minValue) / valueRange) * chartHeight
168-
const y = Math.max(yMin, Math.min(yMax, rawY))
169-
return { x, y }
170-
})
171-
return { ...s, pts }
172-
})
140+
const scaledPoints = useMemo(
141+
() =>
142+
data.map((d, i) => {
143+
const usableW = Math.max(1, chartWidth)
144+
const x = padding.left + (i / (data.length - 1 || 1)) * usableW
145+
const rawY = padding.top + chartHeight - ((d.value - minValue) / valueRange) * chartHeight
146+
const y = Math.max(yMin, Math.min(yMax, rawY))
147+
return { x, y }
148+
}),
149+
[data, chartWidth, chartHeight, minValue, valueRange, yMin, yMax, padding.left, padding.top]
150+
)
151+
152+
const scaledSeries = useMemo(
153+
() =>
154+
allSeries.map((s) => {
155+
const pts = s.data.map((d, i) => {
156+
const usableW = Math.max(1, chartWidth)
157+
const x = padding.left + (i / (s.data.length - 1 || 1)) * usableW
158+
const rawY = padding.top + chartHeight - ((d.value - minValue) / valueRange) * chartHeight
159+
const y = Math.max(yMin, Math.min(yMax, rawY))
160+
return { x, y }
161+
})
162+
return { ...s, pts }
163+
}),
164+
[
165+
allSeries,
166+
chartWidth,
167+
chartHeight,
168+
minValue,
169+
valueRange,
170+
yMin,
171+
yMax,
172+
padding.left,
173+
padding.top,
174+
]
175+
)
173176

174177
const getSeriesById = (id?: string | null) => scaledSeries.find((s) => s.id === id)
175-
const visibleSeries = activeSeriesId
176-
? scaledSeries.filter((s) => s.id === activeSeriesId)
177-
: scaledSeries
178-
const orderedSeries = (() => {
179-
if (!activeSeriesId) return visibleSeries
180-
return visibleSeries
181-
})()
182-
183-
const pathD = (() => {
178+
const visibleSeries = useMemo(
179+
() => (activeSeriesId ? scaledSeries.filter((s) => s.id === activeSeriesId) : scaledSeries),
180+
[activeSeriesId, scaledSeries]
181+
)
182+
183+
const pathD = useMemo(() => {
184184
if (scaledPoints.length <= 1) return ''
185185
const p = scaledPoints
186186
const tension = 0.2
@@ -199,7 +199,7 @@ export function LineChart({
199199
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`
200200
}
201201
return d
202-
})()
202+
}, [scaledPoints, yMin, yMax])
203203

204204
const getCompactDateLabel = (timestamp?: string) => {
205205
if (!timestamp) return ''
@@ -222,6 +222,30 @@ export function LineChart({
222222
const currentHoverDate =
223223
hoverIndex !== null && data[hoverIndex] ? getCompactDateLabel(data[hoverIndex].timestamp) : ''
224224

225+
if (containerWidth === null) {
226+
return (
227+
<div
228+
ref={containerRef}
229+
className={cn('w-full', !hasExternalWrapper && 'rounded-lg border bg-card p-4')}
230+
style={{ height }}
231+
/>
232+
)
233+
}
234+
235+
if (data.length === 0) {
236+
return (
237+
<div
238+
className={cn(
239+
'flex items-center justify-center',
240+
!hasExternalWrapper && 'rounded-lg border bg-card p-4'
241+
)}
242+
style={{ width, height }}
243+
>
244+
<p className='text-muted-foreground text-sm'>No data</p>
245+
</div>
246+
)
247+
}
248+
225249
return (
226250
<div
227251
ref={containerRef}
@@ -386,7 +410,7 @@ export function LineChart({
386410
)
387411
})()}
388412

389-
{orderedSeries.map((s, idx) => {
413+
{visibleSeries.map((s, idx) => {
390414
const isActive = activeSeriesId ? activeSeriesId === s.id : true
391415
const isHovered = hoverSeriesId ? hoverSeriesId === s.id : false
392416
const baseOpacity = isActive ? 1 : 0.12
@@ -682,4 +706,8 @@ export function LineChart({
682706
)
683707
}
684708

709+
/**
710+
* Memoized LineChart component to prevent re-renders when parent updates.
711+
*/
712+
export const LineChart = memo(LineChartComponent)
685713
export default LineChart

apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/status-bar/status-bar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function StatusBar({
3636
const end = new Date(start.getTime() + (segmentDurationMs || 0))
3737
const rangeLabel = Number.isNaN(start.getTime())
3838
? ''
39-
: `${start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric' })}${end.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}`
39+
: `${start.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}${end.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit' })}`
4040
return {
4141
rangeLabel,
4242
successLabel: `${segment.successRate.toFixed(1)}%`,

0 commit comments

Comments
 (0)