diff --git a/studio/frontend/src/components/ui/data-table.tsx b/studio/frontend/src/components/ui/data-table.tsx index 954d2ae0b7..bf2d07021d 100644 --- a/studio/frontend/src/components/ui/data-table.tsx +++ b/studio/frontend/src/components/ui/data-table.tsx @@ -22,15 +22,24 @@ interface DataTableProps { columns: ColumnDef[]; data: TData[]; className?: string; + onRowClick?: (row: TData, rowIndex: number, rowId: string) => void; + getRowClassName?: ( + row: TData, + rowIndex: number, + rowId: string, + ) => string | undefined; } export function DataTable({ columns, data, className, + onRowClick, + getRowClassName, }: DataTableProps) { const [sorting, setSorting] = useState([]); + // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data, columns, @@ -81,7 +90,9 @@ export function DataTable({ ? "bg-background" : "bg-muted/20", "hover:bg-primary/[0.03]", + getRowClassName?.(row.original, idx, row.id), )} + onClick={() => onRowClick?.(row.original, idx, row.id)} > {row.getVisibleCells().map((cell) => ( ([]); - const [expandedDatasetCells, setExpandedDatasetCells] = useState< - Record + const [hiddenDatasetColumnsByExecution, setHiddenDatasetColumnsByExecution] = useState< + Record + >({}); + const [expandedDatasetRowsByExecution, setExpandedDatasetRowsByExecution] = useState< + Record> + >({}); + const [previewDatasetPageByExecution, setPreviewDatasetPageByExecution] = useState< + Record >({}); const terminalRef = useRef(null); + const shouldStickTerminalToBottomRef = useRef(true); const selectedExecution = useMemo( () => executions.find((execution) => execution.id === selectedExecutionId) ?? @@ -231,10 +240,19 @@ export function ExecutionsView({ selectedExecution.recipeSignature !== currentSignature, ); - useEffect(() => { - setHiddenDatasetColumns([]); - setExpandedDatasetCells({}); - }, [selectedExecution?.id]); + const selectedExecutionIdSafe = selectedExecution?.id ?? null; + const hiddenDatasetColumns = useMemo(() => { + if (!selectedExecutionIdSafe) { + return []; + } + return hiddenDatasetColumnsByExecution[selectedExecutionIdSafe] ?? []; + }, [hiddenDatasetColumnsByExecution, selectedExecutionIdSafe]); + const expandedDatasetRows = useMemo(() => { + if (!selectedExecutionIdSafe) { + return {}; + } + return expandedDatasetRowsByExecution[selectedExecutionIdSafe] ?? {}; + }, [expandedDatasetRowsByExecution, selectedExecutionIdSafe]); const datasetColumnNames = useMemo(() => { if (!selectedExecution) { @@ -267,39 +285,22 @@ export function ExecutionsView({ cell: ({ getValue, row }) => { const rawValue = getValue(); const value = formatCellValue(rawValue); - const canExpand = isExpandableCellValue(value); - const cellId = `${row.id}:${name}`; - const expanded = Boolean(expandedDatasetCells[cellId]); + const rowExpanded = Boolean(expandedDatasetRows[row.id]); + const rowHasExpandableCell = visibleDatasetColumnNames.some((columnName) => + isExpandableCellValue(formatCellValue(row.original[columnName])), + ); + const showTruncated = rowHasExpandableCell && !rowExpanded; return (
- {canExpand ? ( - - ) : ( -

{value}

- )} +

+ {showTruncated ? truncateCellValue(value) : value} +

); }, })); - }, [expandedDatasetCells, selectedExecution, visibleDatasetColumnNames]); + }, [expandedDatasetRows, selectedExecution, visibleDatasetColumnNames]); const analysisColumns = useMemo( () => parseAnalysisColumns(selectedExecution?.analysis ?? null), @@ -338,9 +339,37 @@ export function ExecutionsView({ const datasetPage = selectedExecution?.datasetPage ?? 1; const datasetPageSize = selectedExecution?.datasetPageSize ?? 20; const datasetTotal = selectedExecution?.datasetTotal ?? 0; - const totalPages = Math.max(1, Math.ceil(datasetTotal / datasetPageSize)); + const previewPageRaw = selectedExecutionIdSafe + ? previewDatasetPageByExecution[selectedExecutionIdSafe] ?? 1 + : 1; + const previewTotalPages = useMemo(() => { + if (!selectedExecution || selectedExecution.kind !== "preview") { + return 1; + } + return Math.max( + 1, + Math.ceil(selectedExecution.dataset.length / PREVIEW_DATASET_PAGE_SIZE), + ); + }, [selectedExecution]); + const previewPage = Math.min(previewPageRaw, previewTotalPages); + const totalPages = + selectedExecution?.kind === "preview" + ? previewTotalPages + : Math.max(1, Math.ceil(datasetTotal / datasetPageSize)); const canPageDataset = - Boolean(selectedExecution?.jobId) && selectedExecution?.kind === "full"; + selectedExecution?.kind === "preview" || + (selectedExecution?.kind === "full" && Boolean(selectedExecution.jobId)); + const datasetRowsForTable = useMemo(() => { + if (!selectedExecution) { + return []; + } + if (selectedExecution.kind !== "preview") { + return selectedExecution.dataset; + } + const start = (previewPage - 1) * PREVIEW_DATASET_PAGE_SIZE; + return selectedExecution.dataset.slice(start, start + PREVIEW_DATASET_PAGE_SIZE); + }, [previewPage, selectedExecution]); + const currentDatasetPage = selectedExecution?.kind === "preview" ? previewPage : datasetPage; const recordsMetric = useMemo(() => { if (!selectedExecution || selectedExecution.status !== "completed") { return null; @@ -421,16 +450,29 @@ export function ExecutionsView({ if (!selectedExecution) { return null; } - const { dataset, log_lines, ...rest } = selectedExecution; - return rest; + const next = { ...selectedExecution } as Record; + delete next.dataset; + delete next.log_lines; + return next; }, [selectedExecution]); useEffect(() => { if (!terminalRef.current) { return; } + shouldStickTerminalToBottomRef.current = true; terminalRef.current.scrollTop = terminalRef.current.scrollHeight; - }, [selectedExecution?.id, terminalLines.length]); + }, [selectedExecution?.id]); + + useEffect(() => { + if (!terminalRef.current) { + return; + } + if (!shouldStickTerminalToBottomRef.current) { + return; + } + terminalRef.current.scrollTop = terminalRef.current.scrollHeight; + }, [terminalLines.length]); return (
@@ -777,6 +819,13 @@ export function ExecutionsView({
{ + const element = event.currentTarget; + const distanceFromBottom = + element.scrollHeight - element.scrollTop - element.clientHeight; + shouldStickTerminalToBottomRef.current = + distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX; + }} > {terminalLines.length === 0 ? (

@@ -851,12 +900,23 @@ export function ExecutionsView({ { + event.preventDefault(); + }} onCheckedChange={(checked) => { - setHiddenDatasetColumns((current) => { - if (checked) { - return current.filter((name) => name !== columnName); - } - return [...current, columnName]; + const selectedId = selectedExecution?.id; + if (!selectedId) { + return; + } + setHiddenDatasetColumnsByExecution((current) => { + const currentColumns = current[selectedId] ?? []; + const nextColumns = checked + ? currentColumns.filter((name) => name !== columnName) + : [...currentColumns, columnName]; + return { + ...current, + [selectedId]: nextColumns, + }; }); }} > @@ -869,17 +929,27 @@ export function ExecutionsView({ {canPageDataset && selectedExecution && ( <> - Page {datasetPage}/{totalPages} + Page {currentDatasetPage}/{totalPages} @@ -889,10 +959,19 @@ export function ExecutionsView({ variant="outline" disabled={ isExecutionInProgress(selectedExecution.status) || - datasetPage >= totalPages + currentDatasetPage >= totalPages } - onClick={() => - onLoadDatasetPage(selectedExecution.id, datasetPage + 1)} + onClick={() => { + if (selectedExecution.kind === "preview") { + const selectedId = selectedExecution.id; + setPreviewDatasetPageByExecution((current) => ({ + ...current, + [selectedId]: Math.min(totalPages, currentDatasetPage + 1), + })); + return; + } + onLoadDatasetPage(selectedExecution.id, currentDatasetPage + 1); + }} > Next @@ -910,7 +989,39 @@ export function ExecutionsView({

{ + const canExpand = visibleDatasetColumnNames.some((columnName) => + isExpandableCellValue(formatCellValue(row[columnName])), + ); + if (!canExpand) { + return undefined; + } + return cn( + "cursor-pointer", + expandedDatasetRows[rowId] + ? "bg-primary/[0.05]" + : "hover:bg-primary/[0.06]", + ); + }} + onRowClick={(row, _rowIndex, rowId) => { + const canExpand = visibleDatasetColumnNames.some((columnName) => + isExpandableCellValue(formatCellValue(row[columnName])), + ); + if (!canExpand || !selectedExecutionIdSafe) { + return; + } + setExpandedDatasetRowsByExecution((current) => { + const rows = current[selectedExecutionIdSafe] ?? {}; + return { + ...current, + [selectedExecutionIdSafe]: { + ...rows, + [rowId]: !rows[rowId], + }, + }; + }); + }} />
)}