From f5d6b6fec5bf8c7ac69389c13eb0d0c07b5996b3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 15 Apr 2026 12:48:37 -0700 Subject: [PATCH] fix(logs): close sidebar when selected log disappears from filtered list + cleanup Derive sidebar open state from selection validity instead of using a separate useEffect. Also removes unnecessary useMemo/useCallback in non-memo'd components, replaces useEffect with render-time reset in dashboard, fixes CSS tokens, and adds hierarchical query key factory. Co-Authored-By: Claude Opus 4.6 --- .../components/line-chart/line-chart.tsx | 4 +- .../logs/components/dashboard/dashboard.tsx | 13 +- .../components/log-details/log-details.tsx | 16 +- .../logs/components/logs-list/logs-list.tsx | 21 +- .../workflow-selector/workflow-selector.tsx | 4 +- .../notifications/notifications.tsx | 9 +- .../app/workspace/[workspaceId]/logs/logs.tsx | 194 ++++++++---------- apps/sim/hooks/queries/logs.ts | 5 +- apps/sim/stores/logs/store.ts | 1 + 9 files changed, 112 insertions(+), 155 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx index ccdc7ede82a..8534cb8cdb2 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx @@ -241,7 +241,7 @@ function LineChartComponent({ )} style={{ width, height }} > -

No data

+

No data

) } @@ -256,7 +256,7 @@ function LineChartComponent({ > {!hasExternalWrapper && (
-

{label}

+

{label}

{allSeries.length > 1 && (
{scaledSeries.slice(1).map((s) => { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx index f0ff6ca54b7..e69f66e01bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx @@ -1,6 +1,6 @@ 'use client' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useMemo, useRef, useState } from 'react' import { Loader2 } from 'lucide-react' import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' @@ -441,10 +441,13 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) { [] ) - useEffect(() => { - setSelectedSegments((prev) => (Object.keys(prev).length > 0 ? {} : prev)) - setLastAnchorIndices((prev) => (Object.keys(prev).length > 0 ? {} : prev)) - }, [stats, timeRange, workflowIds, searchQuery]) + const resetKey = `${JSON.stringify(stats?.workflows?.map((w) => w.workflowId))}-${timeRange}-${workflowIds.join(',')}-${searchQuery}` + const prevResetKeyRef = useRef(resetKey) + if (resetKey !== prevResetKeyRef.current) { + prevResetKeyRef.current = resetKey + if (Object.keys(selectedSegments).length > 0) setSelectedSegments({}) + if (Object.keys(lastAnchorIndices).length > 0) setLastAnchorIndices({}) + } if (isLoading) { return diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 994b4d7daf0..a48dc3e8a7f 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -296,13 +296,10 @@ export const LogDetails = memo(function LogDetails({ } }, [log?.id]) - const isWorkflowExecutionLog = useMemo(() => { - if (!log) return false - return ( - (log.trigger === 'manual' && !!log.duration) || - (log.executionData?.enhanced && log.executionData?.traceSpans) - ) - }, [log]) + const isWorkflowExecutionLog = + !!log && + ((log.trigger === 'manual' && !!log.duration) || + !!(log.executionData?.enhanced && log.executionData?.traceSpans)) const hasCostInfo = isWorkflowExecutionLog && log?.cost @@ -337,10 +334,7 @@ export const LogDetails = memo(function LogDetails({ return () => window.removeEventListener('keydown', handleKeyDown) }, [isOpen, onClose, hasPrev, hasNext, onNavigatePrev, onNavigateNext]) - const formattedTimestamp = useMemo( - () => (log ? formatDate(log.createdAt) : null), - [log?.createdAt] - ) + const formattedTimestamp = log ? formatDate(log.createdAt) : null const logStatus = getDisplayStatus(log?.status) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx index 3d1aba93508..dab1c288f79 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx @@ -58,19 +58,14 @@ const LogRow = memo( ? DELETED_WORKFLOW_COLOR : log.workflow?.color - const handleClick = useCallback(() => onClick(log), [onClick, log]) - - const handleMouseEnter = useCallback(() => onHover?.(log), [onHover, log]) - - const handleContextMenu = useCallback( - (e: React.MouseEvent) => { - if (onContextMenu) { - e.preventDefault() - onContextMenu(e, log) - } - }, - [onContextMenu, log] - ) + const handleClick = () => onClick(log) + const handleMouseEnter = () => onHover?.(log) + const handleContextMenu = (e: React.MouseEvent) => { + if (onContextMenu) { + e.preventDefault() + onContextMenu(e, log) + } + } return (
{ - return allWorkflows ? [] : selectedIds - }, [allWorkflows, selectedIds]) + const currentValues = allWorkflows ? [] : selectedIds /** * Handle multi-select changes from Combobox. diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx index 113a7405ad2..61ae2bcf235 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx @@ -197,12 +197,9 @@ export const NotificationSettings = memo(function NotificationSettings({ // Show form if user explicitly opened it OR if loading is complete with no subscriptions const displayForm = showForm || (!isLoading && !hasSubscriptions && !editingId) - const getSubscriptionsForTab = useCallback( - (tab: NotificationType) => { - return subscriptions.filter((s) => s.notificationType === tab) - }, - [subscriptions] - ) + const getSubscriptionsForTab = (tab: NotificationType) => { + return subscriptions.filter((s) => s.notificationType === tab) + } const resetForm = useCallback(() => { setFormData({ diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index 58efb79bcca..8a69912ade5 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -568,6 +568,8 @@ export default function Logs() { } }, [selectedLogId, selectedLogIndex]) + const effectiveSidebarOpen = isSidebarOpen && selectedLogIndex !== -1 + const handleRefresh = useCallback(() => { setIsVisuallyRefreshing(true) const timerId = window.setTimeout(() => { @@ -777,7 +779,7 @@ export default function Logs() { () => ( allWorkflowList.map((w) => ({ id: w.id, name: w.name, color: w.color })), - [allWorkflowList] - ) + const workflows = allWorkflowList.map((w) => ({ id: w.id, name: w.name, color: w.color })) + const folderList = Object.values(folders).filter((f) => f.workspaceId === workspaceId) - const folderList = useMemo( - () => Object.values(folders).filter((f) => f.workspaceId === workspaceId), - [folders, workspaceId] - ) - - const selectedStatuses = useMemo((): string[] => { - if (level === 'all' || !level) return [] - return level.split(',').filter(Boolean) - }, [level]) + const selectedStatuses = level === 'all' || !level ? [] : level.split(',').filter(Boolean) const statusOptions: ComboboxOption[] = useMemo( () => @@ -1285,58 +1277,46 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr [] ) - const handleStatusChange = useCallback( - (values: string[]) => { - setLevel(values.length === 0 ? 'all' : values.join(',')) - }, - [setLevel] - ) - - const statusDisplayLabel = useMemo(() => { - if (selectedStatuses.length === 0) return 'Status' - if (selectedStatuses.length === 1) { - const status = statusOptions.find((s) => s.value === selectedStatuses[0]) - return status?.label || '1 selected' - } - return `${selectedStatuses.length} selected` - }, [selectedStatuses, statusOptions]) - - const selectedStatusColor = useMemo(() => { - if (selectedStatuses.length !== 1) return null - const status = selectedStatuses[0] as LogStatus - return STATUS_CONFIG[status]?.color ?? null - }, [selectedStatuses]) - - const workflowOptions: ComboboxOption[] = useMemo( - () => workflows.map((w) => ({ value: w.id, label: w.name, icon: getColorIcon(w.color, true) })), - [workflows] - ) + const handleStatusChange = (values: string[]) => { + setLevel(values.length === 0 ? 'all' : values.join(',')) + } - const workflowDisplayLabel = useMemo(() => { - if (workflowIds.length === 0) return 'Workflow' - if (workflowIds.length === 1) { - const workflow = workflows.find((w) => w.id === workflowIds[0]) - return workflow?.name || '1 selected' - } - return `${workflowIds.length} workflows` - }, [workflowIds, workflows]) + const statusDisplayLabel = + selectedStatuses.length === 0 + ? 'Status' + : selectedStatuses.length === 1 + ? statusOptions.find((s) => s.value === selectedStatuses[0])?.label || '1 selected' + : `${selectedStatuses.length} selected` + + const selectedStatusColor = + selectedStatuses.length === 1 + ? (STATUS_CONFIG[selectedStatuses[0] as LogStatus]?.color ?? null) + : null + + const workflowOptions: ComboboxOption[] = workflows.map((w) => ({ + value: w.id, + label: w.name, + icon: getColorIcon(w.color, true), + })) + + const workflowDisplayLabel = + workflowIds.length === 0 + ? 'Workflow' + : workflowIds.length === 1 + ? workflows.find((w) => w.id === workflowIds[0])?.name || '1 selected' + : `${workflowIds.length} workflows` const selectedWorkflow = workflowIds.length === 1 ? workflows.find((w) => w.id === workflowIds[0]) : null - const folderOptions: ComboboxOption[] = useMemo( - () => folderList.map((f) => ({ value: f.id, label: f.name })), - [folderList] - ) + const folderOptions: ComboboxOption[] = folderList.map((f) => ({ value: f.id, label: f.name })) - const folderDisplayLabel = useMemo(() => { - if (folderIds.length === 0) return 'Folder' - if (folderIds.length === 1) { - const folder = folderList.find((f) => f.id === folderIds[0]) - return folder?.name || '1 selected' - } - return `${folderIds.length} folders` - }, [folderIds, folderList]) + const folderDisplayLabel = + folderIds.length === 0 + ? 'Folder' + : folderIds.length === 1 + ? folderList.find((f) => f.id === folderIds[0])?.name || '1 selected' + : `${folderIds.length} folders` const triggerOptions: ComboboxOption[] = useMemo( () => @@ -1348,69 +1328,57 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr [] ) - const triggerDisplayLabel = useMemo(() => { - if (triggers.length === 0) return 'Trigger' - if (triggers.length === 1) { - const trigger = triggerOptions.find((t) => t.value === triggers[0]) - return trigger?.label || '1 selected' - } - return `${triggers.length} triggers` - }, [triggers, triggerOptions]) - - const timeDisplayLabel = useMemo(() => { - if (timeRange === 'All time') return 'Time' - if (timeRange === 'Custom range' && startDate && endDate) { - return `${formatDateShort(startDate)} - ${formatDateShort(endDate)}` + const triggerDisplayLabel = + triggers.length === 0 + ? 'Trigger' + : triggers.length === 1 + ? triggerOptions.find((t) => t.value === triggers[0])?.label || '1 selected' + : `${triggers.length} triggers` + + const timeDisplayLabel = + timeRange === 'All time' + ? 'Time' + : timeRange === 'Custom range' && startDate && endDate + ? `${formatDateShort(startDate)} - ${formatDateShort(endDate)}` + : timeRange === 'Custom range' + ? 'Custom range' + : timeRange + + const handleTimeRangeChange = (val: string) => { + if (val === 'Custom range') { + setPreviousTimeRange(timeRange) + setDatePickerOpen(true) + } else { + clearDateRange() + setTimeRange(val as typeof timeRange) } - if (timeRange === 'Custom range') return 'Custom range' - return timeRange - }, [timeRange, startDate, endDate]) - - const handleTimeRangeChange = useCallback( - (val: string) => { - if (val === 'Custom range') { - setPreviousTimeRange(timeRange) - setDatePickerOpen(true) - } else { - clearDateRange() - setTimeRange(val as typeof timeRange) - } - }, - [timeRange, setTimeRange, clearDateRange] - ) + } - const handleDateRangeApply = useCallback( - (start: string, end: string) => { - setDateRange(start, end) - setDatePickerOpen(false) - }, - [setDateRange] - ) + const handleDateRangeApply = (start: string, end: string) => { + setDateRange(start, end) + setDatePickerOpen(false) + } - const handleDatePickerCancel = useCallback(() => { + const handleDatePickerCancel = () => { if (timeRange === 'Custom range' && !startDate) { setTimeRange(previousTimeRange) } setDatePickerOpen(false) - }, [timeRange, startDate, previousTimeRange, setTimeRange]) + } - const filtersActive = useMemo( - () => - hasActiveFilters({ - timeRange, - level, - workflowIds, - folderIds, - triggers, - searchQuery, - }), - [timeRange, level, workflowIds, folderIds, triggers, searchQuery] - ) + const filtersActive = hasActiveFilters({ + timeRange, + level, + workflowIds, + folderIds, + triggers, + searchQuery, + }) - const handleClearFilters = useCallback(() => { + const handleClearFilters = () => { resetFilters() onSearchQueryChange('') - }, [resetFilters, onSearchQueryChange]) + } return (
diff --git a/apps/sim/hooks/queries/logs.ts b/apps/sim/hooks/queries/logs.ts index cab3f63ecbd..bbbcea5ba7c 100644 --- a/apps/sim/hooks/queries/logs.ts +++ b/apps/sim/hooks/queries/logs.ts @@ -25,8 +25,9 @@ export const logKeys = { [...logKeys.lists(), workspaceId ?? '', filters] as const, details: () => [...logKeys.all, 'detail'] as const, detail: (logId: string | undefined) => [...logKeys.details(), logId ?? ''] as const, + statsAll: () => [...logKeys.all, 'stats'] as const, stats: (workspaceId: string | undefined, filters: object) => - [...logKeys.all, 'stats', workspaceId ?? '', filters] as const, + [...logKeys.statsAll(), workspaceId ?? '', filters] as const, executionSnapshots: () => [...logKeys.all, 'executionSnapshot'] as const, executionSnapshot: (executionId: string | undefined) => [...logKeys.executionSnapshots(), executionId ?? ''] as const, @@ -327,7 +328,7 @@ export function useCancelExecution() { onSettled: () => { queryClient.invalidateQueries({ queryKey: logKeys.lists() }) queryClient.invalidateQueries({ queryKey: logKeys.details() }) - queryClient.invalidateQueries({ queryKey: [...logKeys.all, 'stats'] }) + queryClient.invalidateQueries({ queryKey: logKeys.statsAll() }) }, }) } diff --git a/apps/sim/stores/logs/store.ts b/apps/sim/stores/logs/store.ts index 99a8cac3b88..f9e0361e2c8 100644 --- a/apps/sim/stores/logs/store.ts +++ b/apps/sim/stores/logs/store.ts @@ -25,6 +25,7 @@ export const useLogDetailsUIStore = create()( }), { name: 'log-details-ui-state', + partialize: (state) => ({ panelWidth: state.panelWidth }), } ) )