Skip to content

Commit 087f52d

Browse files
improvement(table): action bar in mothership + per-execution mode
Three related improvements to the table action bar: 1. Reposition from `position: fixed` to `position: absolute` inside the table's container. Fixed-positioning anchored to the viewport, which centered the bar across the whole window instead of the table panel — wrong in mothership embedded view, where the table sits in the right half. Absolute scopes the bar to the table's bounds. 2. Show the bar for single-execution highlights — when the user selects one workflow-output cell, or 1 row × N cols all within the same workflow group. The bar enters per-execution mode with Run / Stop / View execution buttons targeting that one cell or group. 3. Skip View execution for cancelled cells. A cancelled cell may have been cancelled before the worker ever picked the job up, so its executionId can't be relied on. Tighten the gate everywhere (context menu + action bar) to only `completed` / `error` / `running`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a94efa9 commit 087f52d

3 files changed

Lines changed: 238 additions & 63 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx

Lines changed: 135 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import { AnimatePresence, motion } from 'framer-motion'
44
import { Button, Tooltip } from '@/components/emcn'
5-
import { PlayOutline, RefreshCw, Square } from '@/components/emcn/icons'
5+
import { Eye, PlayOutline, RefreshCw, Square } from '@/components/emcn/icons'
66
import { cn } from '@/lib/core/utils/cn'
77

88
interface TableActionBarProps {
9-
/** Number of rows currently selected (checkbox + multi-row range). */
9+
/** Number of rows currently selected (checkbox + multi-row range). 0 in
10+
* single-cell mode (use `singleCell` instead). */
1011
selectedCount: number
1112
/** Total running/queued workflow cells across the selected rows. Drives the
1213
* Stop button's visibility (hidden when 0) and label. */
@@ -24,6 +25,19 @@ interface TableActionBarProps {
2425
onRerun: () => void
2526
/** Cancel running/queued cells across selected rows. */
2627
onStopWorkflows: () => void
28+
/**
29+
* When the user has a single workflow-output cell highlighted (no row
30+
* selection), the bar switches to a per-cell mode showing the cell's
31+
* status + an Eye button to open the execution log. `null` for multi-row
32+
* selections.
33+
*/
34+
singleCell?: {
35+
canViewExecution: boolean
36+
onViewExecution: () => void
37+
isRunning: boolean
38+
onRunCell: () => void
39+
onStopCell: () => void
40+
} | null
2741
/** Disables actions while a bulk mutation is in flight. */
2842
isLoading?: boolean
2943
/** Additional className for the floating wrapper — used to lift the bar
@@ -32,14 +46,14 @@ interface TableActionBarProps {
3246
}
3347

3448
/**
35-
* Floating action bar shown at the bottom of the viewport when one or more
36-
* rows are selected on a table that has workflow columns. Mirrors the shell
37-
* + interaction pattern from the knowledge-base `<ActionBar>` so the bulk-
38-
* action surface reads consistently across the product.
49+
* Floating action bar shown at the bottom of the table when one or more rows
50+
* are selected, OR when a single workflow-output cell is highlighted. Mirrors
51+
* the shell + interaction pattern from the knowledge-base `<ActionBar>`.
3952
*
40-
* Two run actions: **Play** is the smart default (run only on empty / failed
41-
* cells); **Refresh** forces a full re-run on every selected row. **Stop**
42-
* only appears when ≥1 selected row has a running cell.
53+
* Rendered with `position: absolute` inside the table's container (not
54+
* `fixed`) so it scopes to the table's bounds — important for embedded mode,
55+
* where the table sits inside a panel and a fixed-positioned bar would land
56+
* centered on the whole viewport instead of the panel.
4357
*/
4458
export function TableActionBar({
4559
selectedCount,
@@ -48,10 +62,13 @@ export function TableActionBar({
4862
onRun,
4963
onRerun,
5064
onStopWorkflows,
65+
singleCell,
5166
isLoading = false,
5267
className,
5368
}: TableActionBarProps) {
54-
const visible = hasWorkflowColumns && selectedCount > 0
69+
const isMultiRow = selectedCount > 0
70+
const isSingleCell = !isMultiRow && Boolean(singleCell)
71+
const visible = hasWorkflowColumns && (isMultiRow || isSingleCell)
5572
const stopLabel =
5673
runningCount === 1 ? 'Stop running workflow' : `Stop ${runningCount} running workflows`
5774
const runLabel = 'Run workflows on empty or failed cells'
@@ -67,60 +84,121 @@ export function TableActionBar({
6784
animate={{ opacity: 1, y: 0 }}
6885
exit={{ opacity: 0, y: 10 }}
6986
transition={{ duration: 0.2 }}
70-
className={cn('-translate-x-1/2 fixed bottom-6 z-50 transform', className)}
71-
style={{ left: '50%' }}
87+
className={cn(
88+
'-translate-x-1/2 pointer-events-none absolute bottom-6 left-1/2 z-50 transform',
89+
className
90+
)}
7291
>
73-
<div className='flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5'>
92+
<div className='pointer-events-auto flex items-center gap-2 rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1.5'>
7493
<span className='px-1 text-[var(--text-secondary)] text-small'>
75-
{selectedCount} selected
94+
{isMultiRow ? `${selectedCount} selected` : 'Cell'}
7695
</span>
7796

7897
<div className='flex items-center gap-[5px]'>
79-
<Tooltip.Root>
80-
<Tooltip.Trigger asChild>
81-
<Button
82-
variant='ghost'
83-
onClick={onRun}
84-
disabled={isLoading}
85-
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
86-
aria-label={runLabel}
87-
>
88-
<PlayOutline className='h-[12px] w-[12px]' />
89-
</Button>
90-
</Tooltip.Trigger>
91-
<Tooltip.Content side='top'>{runLabel}</Tooltip.Content>
92-
</Tooltip.Root>
98+
{isMultiRow && (
99+
<>
100+
<Tooltip.Root>
101+
<Tooltip.Trigger asChild>
102+
<Button
103+
variant='ghost'
104+
onClick={onRun}
105+
disabled={isLoading}
106+
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
107+
aria-label={runLabel}
108+
>
109+
<PlayOutline className='h-[12px] w-[12px]' />
110+
</Button>
111+
</Tooltip.Trigger>
112+
<Tooltip.Content side='top'>{runLabel}</Tooltip.Content>
113+
</Tooltip.Root>
93114

94-
<Tooltip.Root>
95-
<Tooltip.Trigger asChild>
96-
<Button
97-
variant='ghost'
98-
onClick={onRerun}
99-
disabled={isLoading}
100-
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
101-
aria-label={rerunLabel}
102-
>
103-
<RefreshCw className='h-[12px] w-[12px]' />
104-
</Button>
105-
</Tooltip.Trigger>
106-
<Tooltip.Content side='top'>{rerunLabel}</Tooltip.Content>
107-
</Tooltip.Root>
115+
<Tooltip.Root>
116+
<Tooltip.Trigger asChild>
117+
<Button
118+
variant='ghost'
119+
onClick={onRerun}
120+
disabled={isLoading}
121+
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
122+
aria-label={rerunLabel}
123+
>
124+
<RefreshCw className='h-[12px] w-[12px]' />
125+
</Button>
126+
</Tooltip.Trigger>
127+
<Tooltip.Content side='top'>{rerunLabel}</Tooltip.Content>
128+
</Tooltip.Root>
108129

109-
{runningCount > 0 && (
110-
<Tooltip.Root>
111-
<Tooltip.Trigger asChild>
112-
<Button
113-
variant='ghost'
114-
onClick={onStopWorkflows}
115-
disabled={isLoading}
116-
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
117-
aria-label={stopLabel}
118-
>
119-
<Square className='h-[12px] w-[12px]' />
120-
</Button>
121-
</Tooltip.Trigger>
122-
<Tooltip.Content side='top'>{stopLabel}</Tooltip.Content>
123-
</Tooltip.Root>
130+
{runningCount > 0 && (
131+
<Tooltip.Root>
132+
<Tooltip.Trigger asChild>
133+
<Button
134+
variant='ghost'
135+
onClick={onStopWorkflows}
136+
disabled={isLoading}
137+
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
138+
aria-label={stopLabel}
139+
>
140+
<Square className='h-[12px] w-[12px]' />
141+
</Button>
142+
</Tooltip.Trigger>
143+
<Tooltip.Content side='top'>{stopLabel}</Tooltip.Content>
144+
</Tooltip.Root>
145+
)}
146+
</>
147+
)}
148+
149+
{isSingleCell && singleCell && (
150+
<>
151+
{!singleCell.isRunning && (
152+
<Tooltip.Root>
153+
<Tooltip.Trigger asChild>
154+
<Button
155+
variant='ghost'
156+
onClick={singleCell.onRunCell}
157+
disabled={isLoading}
158+
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
159+
aria-label='Run cell'
160+
>
161+
<PlayOutline className='h-[12px] w-[12px]' />
162+
</Button>
163+
</Tooltip.Trigger>
164+
<Tooltip.Content side='top'>Run cell</Tooltip.Content>
165+
</Tooltip.Root>
166+
)}
167+
168+
{singleCell.isRunning && (
169+
<Tooltip.Root>
170+
<Tooltip.Trigger asChild>
171+
<Button
172+
variant='ghost'
173+
onClick={singleCell.onStopCell}
174+
disabled={isLoading}
175+
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
176+
aria-label='Stop cell'
177+
>
178+
<Square className='h-[12px] w-[12px]' />
179+
</Button>
180+
</Tooltip.Trigger>
181+
<Tooltip.Content side='top'>Stop cell</Tooltip.Content>
182+
</Tooltip.Root>
183+
)}
184+
185+
{singleCell.canViewExecution && (
186+
<Tooltip.Root>
187+
<Tooltip.Trigger asChild>
188+
<Button
189+
variant='ghost'
190+
onClick={singleCell.onViewExecution}
191+
disabled={isLoading}
192+
className='hover-hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
193+
aria-label='View execution'
194+
>
195+
<Eye className='h-[12px] w-[12px]' />
196+
</Button>
197+
</Tooltip.Trigger>
198+
<Tooltip.Content side='top'>View execution</Tooltip.Content>
199+
</Tooltip.Root>
200+
)}
201+
</>
124202
)}
125203
</div>
126204
</div>

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

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ export interface SelectionSnapshot {
108108
totalRunning: number
109109
/** Whether the table has any workflow-output columns (drives the Run/Stop visibility). */
110110
hasWorkflowColumns: boolean
111+
/**
112+
* When the highlight resolves to exactly one workflow-group execution —
113+
* same row, every highlighted column in the same workflow group — describe
114+
* it so the action bar can offer "View execution" and per-execution run /
115+
* stop. Covers both the 1×1 single-cell case and 1 row × N cols highlights
116+
* within one group. `null` for multi-row, cross-group, or plain-column
117+
* selections.
118+
*/
119+
singleWorkflowCell: {
120+
rowId: string
121+
groupId: string
122+
executionId: string | null
123+
/** True iff the exec is in a state that produced a server log
124+
* (completed / error / running). Drives the View execution button. */
125+
canViewExecution: boolean
126+
/** True iff the exec is currently running or queued. Drives Stop. */
127+
isRunning: boolean
128+
} | null
111129
}
112130

113131
interface TableGridProps {
@@ -649,10 +667,12 @@ export function TableGrid({
649667
return { isWorkflowColumn: false, executionId: null, hasStartedRun: false }
650668
}
651669
const exec = contextMenu.row.executions?.[groupId]
652-
// `queued` / `pending` rows have an executionId reserved but no execution
653-
// row in the logs DB yet — the worker hasn't started, so View execution
654-
// would 404.
655-
const hasStartedRun = exec?.status !== 'queued' && exec?.status !== 'pending'
670+
// Only `completed` / `error` / `running` cells are guaranteed to have a
671+
// server-side execution log. `queued` / `pending` haven't started yet;
672+
// `cancelled` may have been cancelled before the worker ever picked the
673+
// job up, so its executionId can't be relied on either.
674+
const hasStartedRun =
675+
exec?.status === 'completed' || exec?.status === 'error' || exec?.status === 'running'
656676
return {
657677
isWorkflowColumn: true,
658678
executionId: exec?.executionId ?? null,
@@ -2606,6 +2626,39 @@ export function TableGrid({
26062626
0
26072627
)
26082628

2629+
/**
2630+
* Selection that resolves to exactly one workflow-group execution — same
2631+
* row, every highlighted column belonging to the same workflow group. Drives
2632+
* the action bar's per-execution mode (View execution / Run cell / Stop
2633+
* cell). Includes the single-cell case (1×1) and the "highlight a row's
2634+
* workflow outputs" case (1 row × N cols, all in one group). Null for
2635+
* multi-row selections, plain columns, or no selection.
2636+
*/
2637+
const singleWorkflowCell = useMemo<SelectionSnapshot['singleWorkflowCell']>(() => {
2638+
const sel = normalizedSelection
2639+
if (!sel) return null
2640+
if (sel.startRow !== sel.endRow) return null
2641+
const row = rows[sel.startRow]
2642+
if (!row) return null
2643+
const firstCol = displayColumns[sel.startCol]
2644+
const groupId = firstCol?.workflowGroupId
2645+
if (!groupId) return null
2646+
// All columns in the highlight must be in the same workflow group, else
2647+
// we'd be straddling two executions.
2648+
for (let c = sel.startCol + 1; c <= sel.endCol; c++) {
2649+
if (displayColumns[c]?.workflowGroupId !== groupId) return null
2650+
}
2651+
const exec = row.executions?.[groupId]
2652+
const status = exec?.status
2653+
return {
2654+
rowId: row.id,
2655+
groupId,
2656+
executionId: exec?.executionId ?? null,
2657+
canViewExecution: status === 'completed' || status === 'error' || status === 'running',
2658+
isRunning: status === 'running' || status === 'queued' || status === 'pending',
2659+
}
2660+
}, [normalizedSelection, rows, displayColumns])
2661+
26092662
// Emit selection snapshots so the wrapper can render <TableActionBar>.
26102663
// The grid can't fold this into individual event handlers (running counts
26112664
// come from React Query refetches, not user events) so it's intentionally
@@ -2616,8 +2669,19 @@ export function TableGrid({
26162669
const lastSelectionSnapshotRef = useRef<SelectionSnapshot | null>(null)
26172670
useEffect(() => {
26182671
const prev = lastSelectionSnapshotRef.current
2672+
const sameSingleCell =
2673+
(prev?.singleWorkflowCell ?? null) === null && singleWorkflowCell === null
2674+
? true
2675+
: prev?.singleWorkflowCell &&
2676+
singleWorkflowCell &&
2677+
prev.singleWorkflowCell.rowId === singleWorkflowCell.rowId &&
2678+
prev.singleWorkflowCell.groupId === singleWorkflowCell.groupId &&
2679+
prev.singleWorkflowCell.executionId === singleWorkflowCell.executionId &&
2680+
prev.singleWorkflowCell.canViewExecution === singleWorkflowCell.canViewExecution &&
2681+
prev.singleWorkflowCell.isRunning === singleWorkflowCell.isRunning
26192682
if (
26202683
prev &&
2684+
sameSingleCell &&
26212685
prev.runningInActionBarSelection === runningInActionBarSelection &&
26222686
prev.totalRunning === totalRunning &&
26232687
prev.hasWorkflowColumns === hasWorkflowColumns &&
@@ -2631,10 +2695,17 @@ export function TableGrid({
26312695
runningInActionBarSelection,
26322696
totalRunning,
26332697
hasWorkflowColumns,
2698+
singleWorkflowCell,
26342699
}
26352700
lastSelectionSnapshotRef.current = next
26362701
onSelectionChangeRef.current(next)
2637-
}, [actionBarRowIds, runningInActionBarSelection, totalRunning, hasWorkflowColumns])
2702+
}, [
2703+
actionBarRowIds,
2704+
runningInActionBarSelection,
2705+
totalRunning,
2706+
hasWorkflowColumns,
2707+
singleWorkflowCell,
2708+
])
26382709

26392710
const handleRunRow = useCallback(
26402711
(rowId: string) => {

0 commit comments

Comments
 (0)