Skip to content

Commit c7ad09d

Browse files
committed
fix(tables): mount LogView directly for the execution slideout
Step 6. The table borrowed the logs page's `LogDetails` shell to show a run. That shell reads `useParams`, `useRouter`, `useQueryState` and the permission context — four route couplings a table has no use for — and it hard-codes `host='page'`. The `useQueryState` is the interesting one: it deep-links the log's active tab unconditionally, so opening a run from the *tables* page wrote `?tab=trace` onto the tables URL, and onto the host page's URL from the chat panel. A second instance of the same bug step 3 fixed, in a component nobody thought of as URL-owning. `ExecutionSlideout` reproduces only the chrome the table actually uses — frame, resize handle, close button — and mounts the canonical `LogView` itself. It omits `tab`/`onTabChange`, so the view keeps the tab local, which is correct for a panel that was never addressable. The logs page's prev/next and retry controls are dropped deliberately: they navigate a list this surface does not have, and the table only ever passed three of that shell's ten props. `showExecutionInternals` becomes a required prop on the table, forwarded from both hosts. Not read from `usePermissionConfig` inside the table — although R6 would allow it — because passing R6 by an accident of regex coverage is not the same as being right, and a permission-group restriction should fail to compile when a host forgets it rather than default to revealing payloads. Cross-route edges out of the tables tree: 3 left, and only one of them is real.
1 parent f3b47ee commit c7ad09d

6 files changed

Lines changed: 134 additions & 5 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ export const ResourceContent = memo(function ResourceContent({
316316
host='panel'
317317
grants={grants}
318318
onNavigate={navigate}
319+
showExecutionInternals={!permissionConfig.hideTraceSpans}
319320
workspaceId={workspaceId}
320321
tableId={resource.id}
321322
viewsEnabled={tableViewsEnabled}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client'
2+
3+
import { useCallback, useMemo } from 'react'
4+
import { Button, cn } from '@sim/emcn'
5+
import { X } from '@sim/emcn/icons'
6+
import { LogView } from '@/components/resources/log-view'
7+
import type { WorkflowLogRow } from '@/lib/api/contracts/logs'
8+
import { useLogDetailsResize } from '@/hooks/use-log-details-resize'
9+
import { type ResourceGrants, type ResourceHost, workspaceSource } from '@/resources'
10+
import { useLogDetailsUIStore } from '@/stores/logs/store'
11+
import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils'
12+
13+
interface ExecutionSlideoutProps {
14+
log: WorkflowLogRow | null
15+
isOpen: boolean
16+
onClose: () => void
17+
workspaceId: string
18+
grants: ResourceGrants
19+
host: ResourceHost
20+
/**
21+
* Whether this viewer may see trace internals. Required, not
22+
* optional-defaulting-to-true: forgetting it must fail to compile rather than
23+
* silently reveal payloads a permission group hid. Same contract, and the same
24+
* reason, as {@link LogView}'s own prop.
25+
*/
26+
showExecutionInternals: boolean
27+
onNavigate?: (path: string) => void
28+
}
29+
30+
/**
31+
* A workflow run, slid in beside the table that triggered it.
32+
*
33+
* Mounts the canonical {@link LogView} directly rather than borrowing the logs
34+
* page's `LogDetails` shell. That shell reads `useParams`, `useRouter`,
35+
* `useQueryState` and the permission context — four route couplings a table has
36+
* no use for — and it deep-links its active tab, which meant opening a run from
37+
* the tables page wrote `?tab` onto the tables URL, and onto the host page's URL
38+
* from the chat panel. This slideout omits `tab`/`onTabChange` entirely, so the
39+
* view keeps the tab local, which is correct for a panel that was never
40+
* addressable.
41+
*
42+
* Only the chrome is reproduced: the frame, the resize handle, and a close
43+
* button. The logs page's prev/next and retry controls belong to a list this
44+
* surface does not have.
45+
*/
46+
export function ExecutionSlideout({
47+
log,
48+
isOpen,
49+
onClose,
50+
workspaceId,
51+
grants,
52+
host,
53+
showExecutionInternals,
54+
onNavigate,
55+
}: ExecutionSlideoutProps) {
56+
const panelWidth = useLogDetailsUIStore((state) => state.panelWidth)
57+
const { handleMouseDown } = useLogDetailsResize()
58+
59+
const source = useMemo(
60+
() => workspaceSource({ kind: 'log' as const, workspaceId, resourceId: log?.id ?? '' }),
61+
[workspaceId, log?.id]
62+
)
63+
64+
const handleNavigate = useCallback((path: string) => onNavigate?.(path), [onNavigate])
65+
66+
const maxVw = `${MAX_LOG_DETAILS_WIDTH_RATIO * 100}vw`
67+
const effectiveWidth = `clamp(min(${MIN_LOG_DETAILS_WIDTH}px, ${maxVw}), ${panelWidth}px, ${maxVw})`
68+
69+
return (
70+
<>
71+
{isOpen && (
72+
<div
73+
className='absolute top-0 bottom-0 z-[var(--z-dropdown)] w-[8px] cursor-ew-resize'
74+
style={{ right: `calc(${effectiveWidth} - 4px)` }}
75+
onMouseDown={handleMouseDown}
76+
role='separator'
77+
aria-label='Resize execution details panel'
78+
aria-orientation='vertical'
79+
/>
80+
)}
81+
82+
<div
83+
className={cn(
84+
'absolute top-0 right-0 bottom-0 z-[var(--z-dropdown)] overflow-hidden border-l bg-[var(--bg)] shadow-md transition-transform duration-200 ease-out',
85+
isOpen ? 'translate-x-0' : 'translate-x-full'
86+
)}
87+
style={{ width: effectiveWidth }}
88+
aria-label='Execution details sidebar'
89+
>
90+
{log && (
91+
<div className='flex h-full flex-col px-3.5 pt-3'>
92+
<div className='flex items-center justify-between'>
93+
<h2 className='font-medium text-[var(--text-primary)] text-sm'>Execution Details</h2>
94+
<Button variant='ghost' className='!p-1' onClick={onClose} aria-label='Close'>
95+
<X className='size-[14px]' />
96+
</Button>
97+
</div>
98+
99+
<LogView
100+
source={source}
101+
grants={grants}
102+
host={host}
103+
log={log}
104+
showExecutionInternals={showExecutionInternals}
105+
onNavigate={handleNavigate}
106+
/>
107+
</div>
108+
)}
109+
</div>
110+
</>
111+
)
112+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ExecutionSlideout } from './execution-slideout'

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './columns-menu'
33
export * from './context-menu'
44
export * from './enrichment-details'
55
export * from './enrichments-sidebar'
6+
export * from './execution-slideout'
67
export * from './lock-settings-modal'
78
export * from './new-column-dropdown'
89
export * from './row-modal'

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-route.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'
44
import { useParams, useRouter } from 'next/navigation'
55
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
66
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/table'
7+
import { usePermissionConfig } from '@/hooks/use-permission-config'
78
import { grantsFromPermissions } from '@/resources'
89

910
interface TableRouteProps {
@@ -30,6 +31,7 @@ export function TableRoute({ tableLocksEnabled, viewsEnabled }: TableRouteProps)
3031
const tableId = typeof params?.tableId === 'string' ? params.tableId : ''
3132
const permissions = useUserPermissionsContext()
3233
const router = useRouter()
34+
const { config: permissionConfig } = usePermissionConfig()
3335

3436
const grants = useMemo(() => grantsFromPermissions(permissions), [permissions])
3537
const navigate = useCallback((path: string) => router.push(path), [router])
@@ -39,6 +41,7 @@ export function TableRoute({ tableLocksEnabled, viewsEnabled }: TableRouteProps)
3941
host='page'
4042
grants={grants}
4143
onNavigate={navigate}
44+
showExecutionInternals={!permissionConfig.hideTraceSpans}
4245
workspaceId={workspaceId}
4346
tableId={tableId}
4447
tableLocksEnabled={tableLocksEnabled}

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import {
3737
ALL_VIEW_PARAM,
3838
DEFAULT_TABLE_DETAIL_SORT_DIRECTION,
3939
} from '@/lib/table/detail-search-params'
40-
import { LogDetails } from '@/app/workspace/[workspaceId]/logs/components'
4140
import { useLogByExecutionId } from '@/hooks/queries/logs'
4241
import {
4342
downloadTableExport,
@@ -65,6 +64,7 @@ import {
6564
ColumnsMenu,
6665
EnrichmentDetails,
6766
EnrichmentsSidebar,
67+
ExecutionSlideout,
6868
LockSettingsModal,
6969
NewColumnDropdown,
7070
RowModal,
@@ -112,6 +112,13 @@ interface TableProps {
112112
* that owns no router omits this and navigation is inert by construction.
113113
*/
114114
onNavigate?: (path: string) => void
115+
/**
116+
* Whether this viewer may see trace internals, forwarded to the execution
117+
* slideout's {@link LogView}. Required rather than read from
118+
* `usePermissionConfig` here: a permission-group restriction must fail to
119+
* compile when a host forgets it, not default to revealing payloads.
120+
*/
121+
showExecutionInternals: boolean
115122
/**
116123
* The table's address. Required rather than derived: both mounts know it
117124
* (`page.tsx` from its route params, the panel from the open resource), and a
@@ -238,6 +245,7 @@ export function Table({
238245
host,
239246
grants,
240247
onNavigate,
248+
showExecutionInternals,
241249
workspaceId,
242250
tableId,
243251
tableLocksEnabled = false,
@@ -1305,9 +1313,7 @@ export function Table({
13051313
const enrichmentDetailsGroupName =
13061314
enrichmentDetailsTarget &&
13071315
tableWorkflowGroups.find((g) => g.id === enrichmentDetailsTarget.groupId)?.name
1308-
// Fetch the workflow log when the execution-details slideout is open. Reuses
1309-
// the logs page's <LogDetails> directly — no intermediate wrapper needed for
1310-
// a one-line query forward.
1316+
// Fetch the workflow log when the execution-details slideout is open.
13111317
const { data: executionLog } = useLogByExecutionId(workspaceId, executionId)
13121318

13131319
// Stable identity so the memoized Resource.Options can bail — an inline
@@ -1569,10 +1575,15 @@ export function Table({
15691575
tableId={tableId}
15701576
onColumnRename={onColumnRename}
15711577
/>
1572-
<LogDetails
1578+
<ExecutionSlideout
15731579
log={executionLog ?? null}
15741580
isOpen={Boolean(executionId)}
15751581
onClose={onCloseSlideout}
1582+
workspaceId={workspaceId}
1583+
grants={grants}
1584+
host={host}
1585+
showExecutionInternals={showExecutionInternals}
1586+
onNavigate={onNavigate}
15761587
/>
15771588
<EnrichmentDetails
15781589
tableId={tableId}

0 commit comments

Comments
 (0)