mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(tables): resource-cell icons, embedded filters, run-state + UI fixes (#4789)
* fix(tables): resource-cell icons, embedded filters, run-count + queued fixes
- table-grid: render in-workspace resource URLs (workflow/table/KB/file) as
tagged-resource cells reusing ContextMentionIcon (colored square for
workflows), matching @-mention chips; only the matching list is fetched.
- table-grid: fix row-number sticky cell overflow — reserve the full run/stop
button area (30px, not 16px) so wide row indices don't clip.
- table-grid: show an infinite-scroll loading spinner while the next page
loads instead of looking like the end of the table.
- table: surface sort + filter (and run/stop via the options-bar extras slot)
in the embedded mothership table resource view.
- table-grid/utils: stop the dispatch overlay from optimistically painting
autoRun=false cells Queued for auto-fire dispatches — the dispatcher skips
those groups ('autoRun-off'); manual runs still show Queued (manual-bypass).
- dispatcher: exclude orphan pre-stamps (pending + executionId null) from
countRunningCells so the "X running" badge doesn't stick above zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tables): single run/stop control, right-aligned row numbers, View-execution guard
- table: de-duplicate the run/stop control in the embedded mothership view —
drop TableGrid's own embedded run-status bar; it now lives only in the
options bar (left-aligned next to Filter + Sort). Removes the orphaned
RunStatusControl import + onStopAll/cancelRunsPending props.
- data-row: right-align the row number within its box (hugs the right edge,
no hover position jump) with a scaled right inset — 2px for ≤3-digit
indices, 4px for 4+ so narrow columns don't look over-padded.
- table-grid: require a real executionId in the action bar's canViewExecution
flag so an error that never produced an execution (enqueue failure →
status 'error', executionId null) doesn't offer "View execution".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tables): address review — drizzle operators for orphan filter, enabled flag for files query
- dispatcher: replace the `not(and(...)) as SQL` cast in countRunningCells with
`or(ne(status,'pending'), isNotNull(executionId))` — De Morgan equivalent,
fully type-checked, no cast and no hand-written raw SQL.
- workspace-files: add an `enabled` option to useWorkspaceFiles; sim-resource
cell now passes the real workspaceId with `enabled` instead of '' so the
query cache isn't polluted with an empty-key entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tables): "X running" badge counts actual in-flight cells, not dispatch scope
The badge derived from `runningCellCount` (the dispatch-scope estimate =
rows-ahead × groupCount), which over-counts groups that already finished on
rows still inside a dispatch's scope — a cascade where 3 of 4 workflow columns
completed read "4 running" instead of "1". Derive `totalRunning` from the live
`runningByRowId` map instead (the same per-row source the gutter and action-bar
selection already sum), so it reflects cells actually in flight and updates
per-cell via SSE rather than only on dispatch-window events.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fd77bb4069
commit
8d68c8a0f3
+12
-1
@@ -10,6 +10,9 @@ interface CellContentProps {
|
||||
value: unknown
|
||||
exec?: RowExecutionMetadata
|
||||
column: DisplayColumn
|
||||
/** Current workspace id — lets string cells holding an in-workspace resource
|
||||
* URL render as a tagged-resource chip instead of a plain external link. */
|
||||
workspaceId: string
|
||||
isEditing: boolean
|
||||
initialCharacter?: string | null
|
||||
onSave: (value: unknown, reason: SaveReason) => void
|
||||
@@ -34,6 +37,7 @@ export function CellContent({
|
||||
value,
|
||||
exec,
|
||||
column,
|
||||
workspaceId,
|
||||
isEditing,
|
||||
initialCharacter,
|
||||
onSave,
|
||||
@@ -41,7 +45,14 @@ export function CellContent({
|
||||
waitingOnLabels,
|
||||
isEnrichmentOutput,
|
||||
}: CellContentProps) {
|
||||
const kind = resolveCellRender({ value, exec, column, waitingOnLabels, isEnrichmentOutput })
|
||||
const kind = resolveCellRender({
|
||||
value,
|
||||
exec,
|
||||
column,
|
||||
waitingOnLabels,
|
||||
isEnrichmentOutput,
|
||||
currentWorkspaceId: workspaceId,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+72
@@ -9,6 +9,7 @@ import type { RowExecutionMetadata } from '@/lib/table'
|
||||
import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import { storageToDisplay } from '../../../utils'
|
||||
import type { DisplayColumn } from '../types'
|
||||
import { SimResourceCell, type SimResourceType } from './sim-resource-cell'
|
||||
|
||||
export type CellRenderKind =
|
||||
// Workflow-output cells
|
||||
@@ -26,6 +27,13 @@ export type CellRenderKind =
|
||||
| { kind: 'json'; text: string }
|
||||
| { kind: 'date'; text: string }
|
||||
| { kind: 'url'; text: string; href: string; domain: string }
|
||||
| {
|
||||
kind: 'sim-resource'
|
||||
workspaceId: string
|
||||
resourceType: SimResourceType
|
||||
resourceId: string
|
||||
href: string
|
||||
}
|
||||
| { kind: 'text'; text: string }
|
||||
// Universal fallback
|
||||
| { kind: 'empty' }
|
||||
@@ -38,6 +46,9 @@ interface ResolveCellRenderInput {
|
||||
/** Column is an enrichment-group output — a completed-but-empty cell renders
|
||||
* "Not found" rather than a blank, since the enrichment ran and matched nothing. */
|
||||
isEnrichmentOutput?: boolean
|
||||
/** Current workspace id — a URL pointing to a resource in this workspace
|
||||
* renders as a tagged-resource chip rather than a plain external link. */
|
||||
currentWorkspaceId?: string
|
||||
}
|
||||
|
||||
export function resolveCellRender({
|
||||
@@ -46,6 +57,7 @@ export function resolveCellRender({
|
||||
column,
|
||||
waitingOnLabels,
|
||||
isEnrichmentOutput,
|
||||
currentWorkspaceId,
|
||||
}: ResolveCellRenderInput): CellRenderKind {
|
||||
const isNull = value === null || value === undefined
|
||||
const isEmpty = isNull || value === ''
|
||||
@@ -97,6 +109,18 @@ export function resolveCellRender({
|
||||
if (column.type === 'date') return { kind: 'date', text: String(value) }
|
||||
if (column.type === 'string') {
|
||||
const text = stringifyValue(value)
|
||||
if (currentWorkspaceId) {
|
||||
const resource = extractSimResourceInfo(text)
|
||||
if (resource && resource.workspaceId === currentWorkspaceId) {
|
||||
return {
|
||||
kind: 'sim-resource',
|
||||
workspaceId: resource.workspaceId,
|
||||
resourceType: resource.resourceType,
|
||||
resourceId: resource.resourceId,
|
||||
href: resource.href,
|
||||
}
|
||||
}
|
||||
}
|
||||
const urlInfo = extractUrlInfo(text)
|
||||
if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain }
|
||||
return { kind: 'text', text }
|
||||
@@ -131,6 +155,43 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/** Maps a workspace route section to the sim resource kind it addresses. */
|
||||
const SIM_RESOURCE_SECTIONS: Record<string, SimResourceType> = {
|
||||
w: 'workflow',
|
||||
tables: 'table',
|
||||
knowledge: 'knowledge',
|
||||
files: 'file',
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognizes a `/workspace/{id}/{section}/{resourceId}` URL (absolute or
|
||||
* relative) pointing to a sim resource and returns its descriptor. The href is
|
||||
* the pathname so the link stays within the current deployment. Returns null
|
||||
* for anything that isn't a single-segment resource route.
|
||||
*/
|
||||
function extractSimResourceInfo(
|
||||
text: string
|
||||
): { workspaceId: string; resourceType: SimResourceType; resourceId: string; href: string } | null {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return null
|
||||
let pathname: string
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
try {
|
||||
pathname = new URL(trimmed).pathname
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
} else if (trimmed.startsWith('/')) {
|
||||
pathname = trimmed.split(/[?#]/)[0]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
const match = pathname.match(/^\/workspace\/([^/]+)\/(w|tables|knowledge|files)\/([^/]+)\/?$/)
|
||||
if (!match) return null
|
||||
const [, workspaceId, section, resourceId] = match
|
||||
return { workspaceId, resourceType: SIM_RESOURCE_SECTIONS[section], resourceId, href: pathname }
|
||||
}
|
||||
|
||||
interface CellRenderProps {
|
||||
kind: CellRenderKind
|
||||
isEditing: boolean
|
||||
@@ -270,6 +331,17 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
|
||||
</span>
|
||||
)
|
||||
|
||||
case 'sim-resource':
|
||||
return (
|
||||
<SimResourceCell
|
||||
workspaceId={kind.workspaceId}
|
||||
resourceType={kind.resourceType}
|
||||
resourceId={kind.resourceId}
|
||||
href={kind.href}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'text':
|
||||
return (
|
||||
<span
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
|
||||
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
|
||||
/** Sim resource kinds a table cell URL can point to within the current workspace. */
|
||||
export type SimResourceType = 'workflow' | 'table' | 'knowledge' | 'file'
|
||||
|
||||
const FALLBACK_LABEL: Record<SimResourceType, string> = {
|
||||
workflow: 'Workflow',
|
||||
table: 'Table',
|
||||
knowledge: 'Knowledge base',
|
||||
file: 'File',
|
||||
}
|
||||
|
||||
interface SimResourceCellProps {
|
||||
/** Always the current workspace — the resolver only emits this kind for same-workspace URLs. */
|
||||
workspaceId: string
|
||||
resourceType: SimResourceType
|
||||
resourceId: string
|
||||
/** In-app pathname the resource link navigates to. */
|
||||
href: string
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a cell whose value is a URL pointing to a sim resource in the current
|
||||
* workspace as a tagged-resource chip — the same icon (and per-workflow colored
|
||||
* square) used for @-style resource mentions, plus the resource's name as a link.
|
||||
* Only the list matching `resourceType` is fetched; the other queries stay
|
||||
* disabled so a sim-resource cell subscribes to a single shared list.
|
||||
*/
|
||||
export function SimResourceCell({
|
||||
workspaceId,
|
||||
resourceType,
|
||||
resourceId,
|
||||
href,
|
||||
isEditing,
|
||||
}: SimResourceCellProps) {
|
||||
const { data: workflows = [] } = useWorkflows(
|
||||
resourceType === 'workflow' ? workspaceId : undefined
|
||||
)
|
||||
const { data: tables = [] } = useTablesList(resourceType === 'table' ? workspaceId : undefined)
|
||||
const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, {
|
||||
enabled: resourceType === 'knowledge',
|
||||
})
|
||||
const { data: files = [] } = useWorkspaceFiles(workspaceId, 'active', {
|
||||
enabled: resourceType === 'file',
|
||||
})
|
||||
|
||||
const workflow =
|
||||
resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined
|
||||
|
||||
const name = useMemo(() => {
|
||||
switch (resourceType) {
|
||||
case 'workflow':
|
||||
return workflow?.name
|
||||
case 'table':
|
||||
return tables.find((t) => t.id === resourceId)?.name
|
||||
case 'knowledge':
|
||||
return knowledgeBases.find((kb) => kb.id === resourceId)?.name
|
||||
case 'file':
|
||||
return files.find((f) => f.id === resourceId)?.name
|
||||
}
|
||||
}, [resourceType, resourceId, workflow, tables, knowledgeBases, files])
|
||||
|
||||
const label = name ?? FALLBACK_LABEL[resourceType]
|
||||
|
||||
const context: ChatMessageContext =
|
||||
resourceType === 'workflow'
|
||||
? { kind: 'workflow', label, workflowId: resourceId }
|
||||
: resourceType === 'table'
|
||||
? { kind: 'table', label, tableId: resourceId }
|
||||
: resourceType === 'knowledge'
|
||||
? { kind: 'knowledge', label, knowledgeId: resourceId }
|
||||
: { kind: 'file', label, fileId: resourceId }
|
||||
|
||||
return (
|
||||
<span className={cn('flex min-w-0 items-center gap-1.5', isEditing && 'invisible')}>
|
||||
<ContextMentionIcon
|
||||
context={context}
|
||||
workflowColor={workflow?.color ?? null}
|
||||
className='size-[14px] shrink-0 text-[var(--text-icon)]'
|
||||
/>
|
||||
<a
|
||||
href={href}
|
||||
className={cn(
|
||||
'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70',
|
||||
isEditing && 'pointer-events-none'
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
+13
-2
@@ -23,6 +23,9 @@ import { type NormalizedSelection, resolveCellExec } from './utils'
|
||||
export interface DataRowProps {
|
||||
row: TableRowType
|
||||
columns: DisplayColumn[]
|
||||
/** Current workspace id — forwarded to cells so in-workspace resource URLs
|
||||
* render as tagged-resource chips. */
|
||||
workspaceId: string
|
||||
rowIndex: number
|
||||
isFirstRow: boolean
|
||||
editingColumnName: string | null
|
||||
@@ -98,6 +101,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
|
||||
if (
|
||||
prev.row !== next.row ||
|
||||
prev.columns !== next.columns ||
|
||||
prev.workspaceId !== next.workspaceId ||
|
||||
prev.rowIndex !== next.rowIndex ||
|
||||
prev.isFirstRow !== next.isFirstRow ||
|
||||
prev.editingColumnName !== next.editingColumnName ||
|
||||
@@ -141,6 +145,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
|
||||
export const DataRow = React.memo(function DataRow({
|
||||
row,
|
||||
columns,
|
||||
workspaceId,
|
||||
rowIndex,
|
||||
isFirstRow,
|
||||
editingColumnName,
|
||||
@@ -204,7 +209,12 @@ export const DataRow = React.memo(function DataRow({
|
||||
tabIndex={0}
|
||||
aria-checked={isRowSelected}
|
||||
aria-label={`Select row ${rowIndex + 1}`}
|
||||
className='group/checkbox flex h-[20px] shrink-0 items-center justify-center'
|
||||
className={cn(
|
||||
'group/checkbox flex h-[20px] shrink-0 items-center justify-end',
|
||||
// Lighter right inset for narrow indices (≤3 digits → numDivWidth ≤ 28);
|
||||
// full 4px once the column widens (4+ digits, numDivWidth ≥ 36).
|
||||
numDivWidth >= 36 ? 'pr-1' : 'pr-0.5'
|
||||
)}
|
||||
style={{ width: numDivWidth }}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button !== 0) return
|
||||
@@ -216,7 +226,7 @@ export const DataRow = React.memo(function DataRow({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'text-center text-[var(--text-tertiary)] text-xs tabular-nums',
|
||||
'text-right text-[var(--text-tertiary)] text-xs tabular-nums',
|
||||
isRowSelected ? 'hidden' : 'block group-hover/checkbox:hidden'
|
||||
)}
|
||||
>
|
||||
@@ -328,6 +338,7 @@ export const DataRow = React.memo(function DataRow({
|
||||
)}
|
||||
<div className={CELL_CONTENT}>
|
||||
<CellContent
|
||||
workspaceId={workspaceId}
|
||||
value={
|
||||
pendingCellValue && column.name in pendingCellValue
|
||||
? pendingCellValue[column.name]
|
||||
|
||||
+24
-19
@@ -7,7 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { Skeleton, toast, useToast } from '@/components/emcn'
|
||||
import { TableX } from '@/components/emcn/icons'
|
||||
import { Loader, TableX } from '@/components/emcn/icons'
|
||||
import type { RunLimit, RunMode } from '@/lib/api/contracts/tables'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
import type { ColumnConfig } from '../column-config-sidebar'
|
||||
import { ContextMenu } from '../context-menu'
|
||||
import { NewColumnDropdown } from '../new-column-dropdown'
|
||||
import { RunStatusControl } from '../run-status-control'
|
||||
import type { WorkflowConfig } from '../workflow-sidebar'
|
||||
import { ExpandedCellPopover } from './cells'
|
||||
import { ADD_COL_WIDTH, CELL_HEADER_CHECKBOX, COL_WIDTH, SELECTION_TINT_BG } from './constants'
|
||||
@@ -160,10 +159,6 @@ interface TableGridProps {
|
||||
onStopRows: (rowIds: string[]) => void
|
||||
/** Single-row stop for the per-row gutter button. */
|
||||
onStopRow: (rowId: string) => void
|
||||
/** Wholesale cancel — page-header "Stop all". */
|
||||
onStopAll: () => void
|
||||
/** Whether `useCancelTableRuns` is currently in flight. */
|
||||
cancelRunsPending: boolean
|
||||
/**
|
||||
* Fired whenever the action-bar selection or running-count derivations
|
||||
* change. Wrapper uses this to render <TableActionBar>.
|
||||
@@ -258,8 +253,6 @@ export function TableGrid({
|
||||
onRunRows,
|
||||
onStopRows,
|
||||
onStopRow,
|
||||
onStopAll,
|
||||
cancelRunsPending,
|
||||
onSelectionChange,
|
||||
queryOptions,
|
||||
columnRenameSinkRef,
|
||||
@@ -333,8 +326,13 @@ export function TableGrid({
|
||||
|
||||
const { data: tableRunState } = useTableRunState(tableId)
|
||||
const activeDispatches = tableRunState?.dispatches
|
||||
const totalRunning = tableRunState?.runningCellCount ?? 0
|
||||
const runningByRowId = tableRunState?.runningByRowId ?? EMPTY_RUNNING_BY_ROW
|
||||
// Actual in-flight cell count = sum of the live per-row map (kept current by
|
||||
// applyCell's SSE deltas, and the same source the per-row gutter uses). The
|
||||
// dispatch-scope `runningCellCount` over-counts already-completed groups on
|
||||
// rows still inside a dispatch's scope — e.g. a cascade where 3 of 4 columns
|
||||
// finished would read "4 running" instead of "1".
|
||||
const totalRunning = Object.values(runningByRowId).reduce((sum, n) => sum + n, 0)
|
||||
|
||||
const tableRowCountRef = useRef(tableData?.rowCount ?? 0)
|
||||
tableRowCountRef.current = tableData?.rowCount ?? 0
|
||||
@@ -3087,8 +3085,12 @@ export function TableGrid({
|
||||
rowId: row.id,
|
||||
groupId,
|
||||
executionId: exec?.executionId ?? null,
|
||||
// Requires a real executionId: an error that never produced an execution
|
||||
// (e.g. enqueue failure → status 'error' with executionId null) has no
|
||||
// trace to open, so "View execution" must not offer it.
|
||||
canViewExecution:
|
||||
!isEnrichmentGroup &&
|
||||
Boolean(exec?.executionId) &&
|
||||
(status === 'completed' || status === 'error' || status === 'running' || isPaused),
|
||||
}
|
||||
}, [normalizedSelection, rows, displayColumns, workflowGroupById])
|
||||
@@ -3235,16 +3237,6 @@ export function TableGrid({
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='flex h-full flex-col overflow-hidden'>
|
||||
{embedded && totalRunning > 0 && (
|
||||
<div className='flex shrink-0 items-center justify-end border-[var(--border)] border-b px-3 py-1.5'>
|
||||
<RunStatusControl
|
||||
running={totalRunning}
|
||||
onStopAll={onStopAll}
|
||||
isStopping={cancelRunsPending}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='relative flex min-h-0 flex-1'>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
@@ -3498,6 +3490,7 @@ export function TableGrid({
|
||||
key={row.id}
|
||||
row={row}
|
||||
columns={displayColumns}
|
||||
workspaceId={workspaceId}
|
||||
rowIndex={index}
|
||||
isFirstRow={index === 0}
|
||||
editingColumnName={
|
||||
@@ -3541,6 +3534,18 @@ export function TableGrid({
|
||||
/>
|
||||
</tr>
|
||||
)}
|
||||
{isFetchingNextPage && (
|
||||
<tr>
|
||||
<td colSpan={displayColumns.length + 1} className='h-[35px] p-0'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<Loader
|
||||
animate
|
||||
className='size-[14px] shrink-0 text-[var(--text-tertiary)]'
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()
|
||||
|
||||
+10
-1
@@ -49,7 +49,10 @@ export function checkboxColLayout(
|
||||
): { colWidth: number; numDivWidth: number } {
|
||||
const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1
|
||||
const numDivWidth = Math.max(20, digits * 8 + 4)
|
||||
const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 16 : 0)
|
||||
// When workflow columns are present a 20px run/stop button sits to the right of
|
||||
// the number, separated by a 6px gap and a 4px right pad — 30px total. Reserving
|
||||
// only the button width clipped the number on tables with many (wide) row indices.
|
||||
const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 30 : 0)
|
||||
return { colWidth, numDivWidth }
|
||||
}
|
||||
|
||||
@@ -196,6 +199,12 @@ export function resolveCellExec(
|
||||
// cell SSE) cover the actual rows instead.
|
||||
if (d.limit) continue
|
||||
if (!d.scope.groupIds.includes(group.id)) continue
|
||||
// Auto-fire dispatches (row writes / schema changes) scope every group but
|
||||
// the dispatcher honors `autoRun: false` per-cell ('autoRun-off'), so those
|
||||
// cells never actually run — don't optimistically paint them Queued. Manual
|
||||
// runs (Run all / Run column) bypass autoRun and DO run them, so keep the
|
||||
// overlay's Queued there.
|
||||
if (!d.isManualRun && group.autoRun === false) continue
|
||||
if (d.scope.rowIds && !d.scope.rowIds.includes(row.id)) continue
|
||||
if (row.position <= d.cursor) continue
|
||||
return {
|
||||
|
||||
@@ -462,36 +462,46 @@ export function Table({
|
||||
return (
|
||||
<div className='relative flex h-full flex-col overflow-hidden'>
|
||||
{!embedded && (
|
||||
<>
|
||||
<ResourceHeader
|
||||
icon={TableIcon}
|
||||
breadcrumbs={breadcrumbs}
|
||||
createTrigger={createTrigger}
|
||||
actions={headerActions}
|
||||
leadingActions={
|
||||
selection.totalRunning > 0 ? (
|
||||
<RunStatusControl
|
||||
running={selection.totalRunning}
|
||||
onStopAll={onStopAll}
|
||||
isStopping={cancelRunsMutation.isPending}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<ResourceOptionsBar
|
||||
sort={sortConfig}
|
||||
onFilterToggle={() => setFilterOpen((prev) => !prev)}
|
||||
filterActive={filterOpen || !!queryOptions.filter}
|
||||
/>
|
||||
{filterOpen && (
|
||||
<TableFilter
|
||||
columns={columns}
|
||||
filter={queryOptions.filter}
|
||||
onApply={handleFilterApply}
|
||||
onClose={() => setFilterOpen(false)}
|
||||
<ResourceHeader
|
||||
icon={TableIcon}
|
||||
breadcrumbs={breadcrumbs}
|
||||
createTrigger={createTrigger}
|
||||
actions={headerActions}
|
||||
leadingActions={
|
||||
selection.totalRunning > 0 ? (
|
||||
<RunStatusControl
|
||||
running={selection.totalRunning}
|
||||
onStopAll={onStopAll}
|
||||
isStopping={cancelRunsMutation.isPending}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Sort + filter render in both modes. In embedded (mothership) mode there's
|
||||
no ResourceHeader, so the run/stop control rides in the options bar's
|
||||
`extras` slot — keeping the bar populated whether or not a run is live. */}
|
||||
<ResourceOptionsBar
|
||||
sort={sortConfig}
|
||||
onFilterToggle={() => setFilterOpen((prev) => !prev)}
|
||||
filterActive={filterOpen || !!queryOptions.filter}
|
||||
extras={
|
||||
embedded && selection.totalRunning > 0 ? (
|
||||
<RunStatusControl
|
||||
running={selection.totalRunning}
|
||||
onStopAll={onStopAll}
|
||||
isStopping={cancelRunsMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{filterOpen && (
|
||||
<TableFilter
|
||||
columns={columns}
|
||||
filter={queryOptions.filter}
|
||||
onApply={handleFilterApply}
|
||||
onClose={() => setFilterOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<TableGrid
|
||||
workspaceId={workspaceId}
|
||||
@@ -511,8 +521,6 @@ export function Table({
|
||||
onRunRows={onRunRows}
|
||||
onStopRows={onStopRows}
|
||||
onStopRow={onStopRow}
|
||||
onStopAll={onStopAll}
|
||||
cancelRunsPending={cancelRunsMutation.isPending}
|
||||
onSelectionChange={onSelectionChange}
|
||||
queryOptions={queryOptions}
|
||||
columnRenameSinkRef={columnRenameSinkRef}
|
||||
|
||||
@@ -98,11 +98,15 @@ async function fetchWorkspaceFiles(
|
||||
/**
|
||||
* Hook to fetch workspace files
|
||||
*/
|
||||
export function useWorkspaceFiles(workspaceId: string, scope: WorkspaceFileQueryScope = 'active') {
|
||||
export function useWorkspaceFiles(
|
||||
workspaceId: string,
|
||||
scope: WorkspaceFileQueryScope = 'active',
|
||||
options?: { enabled?: boolean }
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: workspaceFilesKeys.list(workspaceId, scope),
|
||||
queryFn: ({ signal }) => fetchWorkspaceFiles(workspaceId, scope, signal),
|
||||
enabled: !!workspaceId,
|
||||
enabled: !!workspaceId && (options?.enabled ?? true),
|
||||
staleTime: 30 * 1000, // 30 seconds - files can change frequently
|
||||
placeholderData: keepPreviousData, // Show cached data immediately
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tableRowExecutions, tableRunDispatches, userTableRows } from '@sim/db/s
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { and, asc, eq, gt, inArray, type SQL, sql } from 'drizzle-orm'
|
||||
import { and, asc, eq, gt, inArray, isNotNull, ne, or, type SQL, sql } from 'drizzle-orm'
|
||||
import { getJobQueue } from '@/lib/core/async-jobs/config'
|
||||
import { writeWorkflowGroupState } from '@/lib/table/cell-write'
|
||||
import { appendTableEvent } from '@/lib/table/events'
|
||||
@@ -185,6 +185,15 @@ export async function insertDispatch(input: {
|
||||
* gutter Run/Stop button. All three statuses are user-cancellable, so the
|
||||
* gutter must surface Stop whenever any of them are present (else clicking
|
||||
* Play during the queued window would re-run an already-queued cell).
|
||||
*
|
||||
* Excludes orphan pre-stamps — `pending` rows with no `executionId` — which
|
||||
* are dead placeholders left when a dispatcher loop wrote the stamp but no
|
||||
* cell-task ever picked it up (lock contention, queue failure, crash). The
|
||||
* cell already shows its prior value and `classifyEligibility` treats these as
|
||||
* claimable, so counting them stuck the "X running" badge above zero forever
|
||||
* even though nothing was running. Same `executionId == null` test used by
|
||||
* {@link classifyEligibility} / {@link pickNextEligibleGroupForRow}.
|
||||
*
|
||||
* Hits the `(table_id, status)` partial index on table_row_executions. */
|
||||
export async function countRunningCells(
|
||||
tableId: string
|
||||
@@ -198,7 +207,10 @@ export async function countRunningCells(
|
||||
.where(
|
||||
and(
|
||||
eq(tableRowExecutions.tableId, tableId),
|
||||
inArray(tableRowExecutions.status, ['queued', 'running', 'pending'])
|
||||
inArray(tableRowExecutions.status, ['queued', 'running', 'pending']),
|
||||
// Exclude orphan pre-stamps (`pending` + null executionId). De Morgan of
|
||||
// NOT(pending AND null) — `status` is NOT NULL so `ne` is well-defined.
|
||||
or(ne(tableRowExecutions.status, 'pending'), isNotNull(tableRowExecutions.executionId))
|
||||
)
|
||||
)
|
||||
.groupBy(tableRowExecutions.rowId)
|
||||
|
||||
Reference in New Issue
Block a user