Skip to content

Commit dfe3619

Browse files
committed
fix(logs): add logs-page Suspense boundary and co-locate nuqs params
- Wrap <Logs/> in <Suspense fallback={<LogsLoading/>}> so the nuqs reads (useLogFilters, executionId) have a boundary ancestor like sibling pages. - Co-locate the executionId param in logs/search-params.ts (read-only, intentionally not stripped) and consume it in logs.tsx. - Migrate log-details activeTab to a deep-linkable nuqs tab param (single LogDetails instance; preview path uses ExecutionSnapshot, not LogDetails). - Align cleanup.md description pass order with the numbered steps. - Replace mothership.tsx local Tab type with exported MothershipTab.
1 parent 0bbb633 commit dfe3619

6 files changed

Lines changed: 61 additions & 15 deletions

File tree

.claude/commands/cleanup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: Run all code quality skills in sequence — effects, memo, callbacks, state, url-state, React Query, and emcn design review
2+
description: Run all code quality skills in sequence — effects, memo, callbacks, state, React Query, emcn design review, and url-state
33
argument-hint: [scope] [fix=true|false]
44
---
55

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
44
import { formatDuration } from '@sim/utils/formatting'
55
import { ArrowDown, ArrowUp, Check, ChevronUp, Clipboard, Search, X } from 'lucide-react'
6+
import { useQueryState } from 'nuqs'
67
import { createPortal } from 'react-dom'
78
import {
89
Button,
@@ -34,6 +35,10 @@ import {
3435
TraceView,
3536
} from '@/app/workspace/[workspaceId]/logs/components'
3637
import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks'
38+
import {
39+
logDetailsTabParam,
40+
logDetailsTabUrlKeys,
41+
} from '@/app/workspace/[workspaceId]/logs/search-params'
3742
import {
3843
DELETED_WORKFLOW_LABEL,
3944
formatDate,
@@ -262,23 +267,22 @@ interface LogDetailsContentProps {
262267

263268
export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentProps) {
264269
const [isExecutionSnapshotOpen, setIsExecutionSnapshotOpen] = useState(false)
265-
const [activeTab, setActiveTab] = useState<LogDetailsTab>('overview')
266-
const [prevLogId, setPrevLogId] = useState(log.id)
270+
const [activeTab, setActiveTab] = useQueryState(logDetailsTabParam.key, {
271+
...logDetailsTabParam.parser,
272+
...logDetailsTabUrlKeys,
273+
})
267274
const { copied: copiedRunId, copy: copyRunId } = useCopyToClipboard({ resetMs: 1500 })
268275

269-
if (prevLogId !== log.id) {
270-
setPrevLogId(log.id)
271-
setActiveTab('overview')
272-
}
273-
274276
const scrollAreaRef = useRef<HTMLDivElement>(null)
275277

276278
const { config: permissionConfig } = usePermissionConfig()
277279

278280
useEffect(() => {
281+
setActiveTab('overview')
279282
if (scrollAreaRef.current) {
280283
scrollAreaRef.current.scrollTop = 0
281284
}
285+
// eslint-disable-next-line react-hooks/exhaustive-deps -- stable nuqs setter; reset tab on log change
282286
}, [log.id])
283287

284288
const isLikelyExecution = !!log.executionId && log.trigger !== 'mothership'

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { formatDuration } from '@sim/utils/formatting'
1313
import { useQueryClient } from '@tanstack/react-query'
1414
import { useParams } from 'next/navigation'
15-
import { parseAsString, useQueryState } from 'nuqs'
15+
import { useQueryState } from 'nuqs'
1616
import {
1717
Button,
1818
ChipCombobox,
@@ -54,6 +54,7 @@ import type {
5454
import { Resource } from '@/app/workspace/[workspaceId]/components'
5555
import { useLogFilters } from '@/app/workspace/[workspaceId]/logs/hooks/use-log-filters'
5656
import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
57+
import { executionIdParam } from '@/app/workspace/[workspaceId]/logs/search-params'
5758
import type { Suggestion } from '@/app/workspace/[workspaceId]/logs/types'
5859
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
5960
import { getBlock } from '@/blocks/registry'
@@ -229,7 +230,7 @@ export default function Logs() {
229230
isSidebarOpen: false,
230231
})
231232

232-
const [executionId] = useQueryState('executionId', parseAsString)
233+
const [executionId] = useQueryState(executionIdParam.key, executionIdParam.parser)
233234
const [pendingExecutionId, setPendingExecutionId] = useState<string | null>(() => executionId)
234235

235236
const [searchQuery, setSearchQuery] = useState(urlSearchQuery)
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1+
import { Suspense } from 'react'
12
import type { Metadata } from 'next'
3+
import LogsLoading from '@/app/workspace/[workspaceId]/logs/loading'
24
import Logs from '@/app/workspace/[workspaceId]/logs/logs'
35

46
export const metadata: Metadata = {
57
title: 'Logs',
68
}
79

8-
export default Logs
10+
/**
11+
* Logs page entry. `Logs` reads URL query params via nuqs (which uses
12+
* `useSearchParams` internally), so it must sit under a Suspense boundary. The
13+
* fallback renders the real chrome so a suspend never shows a blank frame; the
14+
* route-level `loading.tsx` covers the navigation/chunk-load transition.
15+
*/
16+
export default function LogsPage() {
17+
return (
18+
<Suspense fallback={<LogsLoading />}>
19+
<Logs />
20+
</Suspense>
21+
)
22+
}

apps/sim/app/workspace/[workspaceId]/logs/search-params.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createParser, parseAsArrayOf, parseAsString } from 'nuqs/server'
1+
import { createParser, parseAsArrayOf, parseAsString, parseAsStringLiteral } from 'nuqs/server'
22
import {
33
CORE_TRIGGER_TYPES,
44
type LogLevel,
@@ -112,3 +112,31 @@ export const logFilterUrlKeys = {
112112
history: 'replace',
113113
clearOnDefault: true,
114114
} as const
115+
116+
/**
117+
* Read-only deep link to a specific execution. Resolves to a log row and opens
118+
* the details sidebar on load. Intentionally NOT stripped — the link stays
119+
* shareable — so it carries no `clearOnDefault`/`history` options here.
120+
*/
121+
export const executionIdParam = {
122+
key: 'executionId',
123+
parser: parseAsString,
124+
} as const
125+
126+
const LOG_DETAILS_TABS = ['overview', 'trace'] as const
127+
128+
/**
129+
* Active tab of the log-details sidebar (`overview` / `trace`). Deep-linkable so
130+
* a shared link can land on the trace view; `replace` keeps it off the back
131+
* stack and `clearOnDefault` drops it from the URL when on the default tab.
132+
*/
133+
export const logDetailsTabParam = {
134+
key: 'tab',
135+
parser: parseAsStringLiteral(LOG_DETAILS_TABS).withDefault('overview'),
136+
} as const
137+
138+
/** Tab change is view-state, not a destination: replace, clean URL on default. */
139+
export const logDetailsTabUrlKeys = {
140+
history: 'replace',
141+
clearOnDefault: true,
142+
} as const

apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type BYOKManagerProvider,
1212
} from '@/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager'
1313
import {
14+
type MothershipTab,
1415
mothershipParsers,
1516
mothershipUrlKeys,
1617
} from '@/app/workspace/[workspaceId]/settings/components/mothership/search-params'
@@ -43,9 +44,7 @@ const ENTERPRISE_BYOK_PROVIDERS: BYOKManagerProvider[] = [
4344
},
4445
]
4546

46-
type Tab = 'overview' | 'licenses' | 'byok'
47-
48-
const TABS: { id: Tab; label: string }[] = [
47+
const TABS: { id: MothershipTab; label: string }[] = [
4948
{ id: 'overview', label: 'Overview' },
5049
{ id: 'licenses', label: 'Licenses' },
5150
{ id: 'byok', label: 'BYOK' },

0 commit comments

Comments
 (0)