LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_result - data_table.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 85.7 % 335 287
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : import * as React from 'react';
       3              : import * as styles from './data_table.module.css';
       4              : 
       5              : import { Grid, useGridCallbackRef } from 'react-window';
       6              : 
       7              : import { arrowValueToJson, ArrowTableFormatter, isArrowStructuredType } from '../../compute/arrow_formatter.js';
       8              : import { ComputationAction, TableComputationState } from '../../compute/computation_state.js';
       9              : import { Dispatch } from '../../utils/variant.js';
      10              : import { DataCell, DataCellData, TableColumnHeader } from './data_table_cell.js';
      11              : import { classNames } from '../../utils/classnames.js';
      12              : import { computeTableLayout, DataTableLayout } from './data_table_layout.js';
      13              : import { useCrossFilters } from './use_cross_filters.js';
      14              : import { observeSize } from '../../ui/foundations/size_observer.js';
      15              : import { CellDetailOverlay } from './cell_detail_overlay.js';
      16              : import { useAppConfig } from '../../config/app_config.js';
      17              : import { useLogger } from '../../platform/logger/logger_provider.js';
      18              : import { useScrollbarHeight } from '../../utils/scrollbar.js';
      19              : import { useDataTableOrdering } from './data_table_ordering.js';
      20              : import { useDataTablePortalContainers } from './data_table_portals.js';
      21              : import { ResizedColumnWidths, useDataTableColumnResize } from './data_table_column_resize.js';
      22              : import { DataTableStickyColumn, DataTableStickyHeaders } from './data_table_sticky_views.js';
      23              : import { DataTableFocusStore } from './data_table_focus.js';
      24              : import { resolveSearchedRowIndices } from './visible_rows.js';
      25              : import { collectSearchableResultColumns } from '../../compute/sql/query_result_search_sql.js';
      26              : 
      27              : const LOG_CTX = 'data_table';
      28              : 
      29              : interface Props {
      30              :     className?: string;
      31              :     table: TableComputationState;
      32              :     dispatchComputation: Dispatch<ComputationAction>;
      33              :     debugMode: boolean;
      34              :     maxRows?: number;
      35              :     columnHeader?: TableColumnHeader;
      36              :     cellBackground?: string;
      37              :     onShowTable?: () => void;
      38              :     fitHeight?: boolean;
      39              :     /// The grid takes the height of its host however few rows it shows, the last of `maxRows` may be cut off
      40              :     fillHeight?: boolean;
      41              :     maxHeight?: number;
      42              :     compact?: boolean;
      43              : }
      44              : 
      45            1 : const MIN_GRID_HEIGHT = 200;
      46            1 : const MIN_GRID_WIDTH = 100;
      47            1 : const COLUMN_HEADER_HEIGHT = 32;
      48            1 : const COLUMN_HEADER_PLOTS_HEIGHT = 76;
      49              : /// The row of the column plots shrinks to a strip that still holds its toggle
      50            1 : const COLUMN_HEADER_PLOTS_COLLAPSED_HEIGHT = 18;
      51            1 : const ROW_HEIGHT = 26;
      52            1 : const GRID_OVERSCAN_COUNT = 10;
      53            1 : const NOOP_BRUSHING = () => { };
      54              : 
      55            1 : export function computeGridHeight(
      56            1 :     baseRowCount: number,
      57            1 :     visibleRowCount: number,
      58            1 :     headerHeight: number,
      59            1 :     scrollbarHeight: number,
      60            1 :     fitHeight: boolean,
      61            1 :     maxHeight?: number,
      62            1 :     measuredHeight: number = 0,
      63            1 :     maxRows?: number,
      64            1 : ): number {
      65            1 :     const contentHeight = headerHeight + visibleRowCount * ROW_HEIGHT + scrollbarHeight;
      66            1 :     if (fitHeight) {
      67            1 :         const fittedRowCount = maxRows == null ? baseRowCount : Math.min(baseRowCount, maxRows);
      68            1 :         return Math.min(
      69            1 :             headerHeight + fittedRowCount * ROW_HEIGHT + scrollbarHeight,
      70            1 :             maxHeight ?? Number.POSITIVE_INFINITY,
      71            1 :         );
      72              :     }
      73            1 :     return maxRows != null ? contentHeight : Math.max(measuredHeight, MIN_GRID_HEIGHT);
      74            1 : }
      75              : 
      76              : /// The rows of a table with a column header of names that show in a height, at least one. The last row may be cut
      77              : /// off, the table then ends at the bottom of the height and not a fraction of a row above it. A horizontal scrollbar
      78              : /// covers the bottom of the rows.
      79            1 : export function fittingRowCount(height: number): number {
      80            1 :     return Math.max(1, Math.ceil((height - COLUMN_HEADER_HEIGHT) / ROW_HEIGHT));
      81            1 : }
      82              : 
      83              : interface RenderedCells {
      84              :     rowStart: number;
      85              :     rowStop: number;
      86              :     columnStart: number;
      87              :     columnStop: number;
      88              : }
      89              : 
      90            1 : export const DataTable = React.memo((props: Props) => {
      91            1 :     const config = useAppConfig();
      92            1 :     const logger = useLogger();
      93            1 :     const dispatchComputation = props.dispatchComputation;
      94            1 :     const computationState = props.table;
      95            1 :     const dataTable = computationState.dataTable;
      96            1 :     const [gridApi, setGridApi] = useGridCallbackRef(null);
      97            1 :     const gridContainerElement = React.useRef(null);
      98            1 :     const gridContainerSize = observeSize(gridContainerElement);
      99            1 :     const gridContainerWidth = Math.max(gridContainerSize?.width ?? 0, MIN_GRID_WIDTH);
     100            1 :     const columnHeader = props.columnHeader ?? ((config?.settings?.enableTableColumnPlots ?? false)
     101            0 :         ? TableColumnHeader.WithColumnPlots
     102            0 :         : TableColumnHeader.OnlyColumnName);
     103            1 :     const [plotsCollapsed, setPlotsCollapsed] = React.useState(false);
     104            1 :     const togglePlots = React.useCallback(() => setPlotsCollapsed(c => !c), []);
     105            1 :     const plotsHeight = plotsCollapsed ? COLUMN_HEADER_PLOTS_COLLAPSED_HEIGHT : COLUMN_HEADER_PLOTS_HEIGHT;
     106              : 
     107              :     // Get the row-id indirection column from ordering or filtering
     108            1 :     const visibleRowIndices = React.useMemo(
     109            1 :         () => resolveSearchedRowIndices(computationState),
     110            1 :         [computationState.orderingTable, computationState.filterTable, computationState.dataSearchTable, computationState.dataSearch.matchingRows],
     111            1 :     );
     112              : 
     113              :     // Data row count. Headers are rendered separately via portals
     114              :     // When an indirection table is active, show only the derived visible rows
     115            1 :     const totalRowCount = visibleRowIndices?.length ?? dataTable.numRows ?? 0;
     116            1 :     const dataRowCount = props.maxRows != null ? Math.min(totalRowCount, props.maxRows) : totalRowCount;
     117              :     // Header configuration
     118            1 :     const headerRowCount = columnHeader === TableColumnHeader.WithColumnPlots ? 2 : 1;
     119            1 :     const visibleColumnGroups = React.useMemo(() => {
     120            1 :         const matches = computationState.columnSearch.matchingColumnGroups;
     121            1 :         return matches == null ? null : new Set(matches);
     122            1 :     }, [computationState.columnSearch.matchingColumnGroups]);
     123            1 :     const searchColumnIndexByGroup = React.useMemo(() => new Map(
     124            1 :         collectSearchableResultColumns(computationState.columnGroups)
     125            1 :             .map(column => [column.columnGroupIdx, column.columnIdx] as const),
     126            1 :     ), [computationState.columnGroups]);
     127              : 
     128              :     // Construct the arrow formatter and update it whenever the data table changes
     129            1 :     const tableFormatter = React.useMemo(() => {
     130            1 :         return new ArrowTableFormatter(dataTable.schema, dataTable.batches, logger);
     131            1 :     }, [dataTable]);
     132              : 
     133              :     // Columns that the user resized by dragging a border, kept while the table is mounted
     134            1 :     const [resizedColumnWidths, setResizedColumnWidths] = React.useState<ResizedColumnWidths>(() => new Map());
     135              : 
     136              :     // Determine grid dimensions and column widths
     137            1 :     const gridLayout = React.useMemo<DataTableLayout>(() => {
     138            1 :         if (!tableFormatter) {
     139            0 :             return {
     140            0 :                 columnCount: 0,
     141            0 :                 arrowFieldByColumnIndex: new Uint32Array(),
     142            0 :                 columnXOffsets: new Float64Array([0]),
     143            0 :                 columnAggregateByColumnIndex: new Int32Array(),
     144            0 :                 columnGroupByColumnIndex: new Uint32Array(),
     145            0 :                 isSystemColumn: new Uint8Array(),
     146            0 :                 isTextColumn: new Uint8Array(),
     147            0 :                 headerRowCount
     148            0 :             };
     149              :         }
     150            1 :         return computeTableLayout(tableFormatter, computationState, props.debugMode, headerRowCount, gridContainerWidth, visibleColumnGroups, resizedColumnWidths);
     151            1 :     }, [
     152            1 :         computationState.columnGroups,
     153            1 :         tableFormatter,
     154            1 :         props.debugMode,
     155            1 :         gridContainerWidth,
     156            1 :         headerRowCount,
     157            1 :         visibleColumnGroups,
     158            1 :         resizedColumnWidths,
     159            1 :     ]);
     160              : 
     161              :     // Compute helper to resolve column widths
     162            1 :     const getColumnWidth = React.useCallback((column: number) =>
     163            1 :         gridLayout.columnXOffsets[column + 1] - gridLayout.columnXOffsets[column],
     164            1 :         [gridLayout]);
     165              : 
     166              :     // Grid re-renders automatically when cellProps (gridData) changes
     167              :     // which includes gridLayout as a dependency
     168              : 
     169              :     // Track react-window's already-overscanned range for the sticky header and column.
     170            1 :     const [renderedCells, setRenderedCells] = React.useState<RenderedCells>({
     171            1 :         rowStart: 0,
     172            1 :         rowStop: 0,
     173            1 :         columnStart: 0,
     174            1 :         columnStop: 0,
     175            1 :     });
     176              : 
     177              :     // Shared cross-filter controller: selection lives on the computation state and drives
     178              :     // a single filterTable. Also owns the guarded filtering effect.
     179            1 :     const { crossFilters, histogramFilter, mostFrequentValueFilter, requestFilteredColumnAggregation } = useCrossFilters(
     180            1 :         computationState,
     181            1 :         dispatchComputation,
     182            1 :         gridLayout,
     183            1 :     );
     184              : 
     185            1 :     const { orderByColumn, getSortDirection } = useDataTableOrdering(computationState, dispatchComputation);
     186              : 
     187              :     // Maintain the focused cell outside of React state, cells subscribe to their own row
     188            1 :     const [focusStore] = React.useState(() => new DataTableFocusStore());
     189            1 :     const onMouseEnterCell: React.PointerEventHandler<HTMLDivElement> = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
     190            0 :         const tableRow = Number.parseInt(event.currentTarget.dataset["tableRow"]!);
     191            0 :         const tableCol = Number.parseInt(event.currentTarget.dataset["tableCol"]!);
     192            0 :         focusStore.setFocus(tableRow, tableCol);
     193            1 :     }, [focusStore]);
     194            1 :     const onMouseLeaveCell: React.PointerEventHandler<HTMLDivElement> = React.useCallback((_event: React.PointerEvent<HTMLDivElement>) => {
     195            0 :         focusStore.setFocus(null, null);
     196            1 :     }, [focusStore]);
     197              : 
     198              :     // Cell detail overlay state
     199            1 :     const [cellDetail, setCellDetail] = React.useState<{
     200              :         visibleRow: number;
     201              :         dataRow: number;
     202              :         fieldId: number;
     203              :         formattedValue: string | null;
     204              :         structuredValue: object | null;
     205              :         columnName: string | null;
     206            1 :     } | null>(null);
     207            1 :     const onClickCell: React.MouseEventHandler<HTMLDivElement> = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
     208            0 :         const visibleRow = Number.parseInt(event.currentTarget.dataset["visibleRow"]!);
     209            0 :         const dataRow = Number.parseInt(event.currentTarget.dataset["tableRow"]!);
     210            0 :         const fieldId = Number.parseInt(event.currentTarget.dataset["tableCol"]!);
     211            0 :         const field = computationState.dataTable.schema.fields[fieldId];
     212            0 :         if (field == null) return;
     213            0 :         const isText = field.type.typeId === arrow.Type.Utf8 || field.type.typeId === arrow.Type.LargeUtf8;
     214            0 :         const isStructured = isArrowStructuredType(field.type);
     215            0 :         if (!isText && !isStructured) return;
     216            0 :         const formattedValue = tableFormatter.getValue(dataRow, fieldId);
     217            0 :         const rawValue = isStructured ? dataTable.getChildAt(fieldId)?.get(dataRow) : null;
     218            0 :         const structuredValue = rawValue == null ? null : arrowValueToJson(rawValue) as object;
     219            0 :         const columnName = field.name ?? null;
     220            0 :         setCellDetail({ visibleRow, dataRow, fieldId, formattedValue, structuredValue, columnName });
     221            1 :     }, [tableFormatter, dataTable]);
     222            1 :     const onNavigateCell = React.useCallback((delta: number) => {
     223            0 :         setCellDetail(prev => {
     224            0 :             if (prev == null) return null;
     225            0 :             const visibleRow = prev.visibleRow + delta;
     226            0 :             if (visibleRow < 0 || visibleRow >= dataRowCount) return prev;
     227            0 :             const dataRow = visibleRowIndices == null
     228            0 :                 ? visibleRow
     229            0 :                 : visibleRowIndices[visibleRow];
     230            0 :             const formattedValue = tableFormatter.getValue(dataRow, prev.fieldId);
     231            0 :             const field = dataTable.schema.fields[prev.fieldId];
     232            0 :             const isStructured = isArrowStructuredType(field.type);
     233            0 :             const rawValue = isStructured ? dataTable.getChildAt(prev.fieldId)?.get(dataRow) : null;
     234            0 :             const structuredValue = rawValue == null ? null : arrowValueToJson(rawValue) as object;
     235            0 :             return { ...prev, visibleRow, dataRow, formattedValue, structuredValue };
     236            0 :         });
     237            1 :     }, [tableFormatter, dataTable, dataRowCount, visibleRowIndices]);
     238              : 
     239              :     // Maintain a rendering context for data cells.
     240              :     // This context is passed to grid elements as item data.
     241            1 :     const gridData = React.useMemo<DataCellData>(() => ({
     242            1 :         visibleRowIndices,
     243            1 :         gridLayout: gridLayout,
     244            1 :         hideRowHeader: true,
     245            1 :         columnGroups: computationState.columnGroups,
     246            1 :         tableFormatter: tableFormatter,
     247            1 :         onMouseEnter: onMouseEnterCell,
     248            1 :         onMouseLeave: onMouseLeaveCell,
     249            1 :         onClick: onClickCell,
     250            1 :         table: computationState.dataTable,
     251            1 :         focus: focusStore,
     252            1 :         rightmostVisibleColumn: gridLayout.columnCount - 1,
     253            1 :         matchingRows: computationState.dataSearch.matchingRows,
     254            1 :         searchColumnIndexByGroup,
     255            1 :     }), [
     256              :         // Data dependencies that legitimately require cell re-renders
     257            1 :         computationState.columnGroups,
     258            1 :         computationState.dataTable,
     259            1 :         visibleRowIndices,
     260            1 :         gridLayout,
     261            1 :         tableFormatter,
     262            1 :         computationState.dataSearch.matchingRows,
     263            1 :         searchColumnIndexByGroup,
     264              :         // Stable callbacks - included for correctness but won't cause re-renders
     265            1 :         focusStore,
     266            1 :         onMouseEnterCell,
     267            1 :         onMouseLeaveCell,
     268            1 :         onClickCell,
     269            1 :     ]);
     270              :     // Track only range boundary changes, rather than updating React state for every scroll event.
     271            1 :     const onCellsRendered = React.useCallback((
     272            1 :         _visibleCells: { columnStartIndex: number; columnStopIndex: number; rowStartIndex: number; rowStopIndex: number },
     273            1 :         allCells: { columnStartIndex: number; columnStopIndex: number; rowStartIndex: number; rowStopIndex: number }
     274            1 :     ) => {
     275            1 :         setRenderedCells(prev => {
     276            1 :             if (
     277            1 :                 prev.rowStart === allCells.rowStartIndex
     278            1 :                 && prev.rowStop === allCells.rowStopIndex
     279            1 :                 && prev.columnStart === allCells.columnStartIndex
     280            1 :                 && prev.columnStop === allCells.columnStopIndex
     281              :             ) {
     282            1 :                 return prev;
     283              :             }
     284            1 :             return {
     285            1 :                 rowStart: allCells.rowStartIndex,
     286            1 :                 rowStop: allCells.rowStopIndex,
     287            1 :                 columnStart: allCells.columnStartIndex,
     288            1 :                 columnStop: allCells.columnStopIndex,
     289            1 :             };
     290            1 :         });
     291            1 :     }, []);
     292              : 
     293              :     // Compute grid dimensions
     294            1 :     const totalColumnsWidth = gridLayout.columnXOffsets[gridLayout.columnCount] ?? 0;
     295            1 :     const firstColumnWidth = getColumnWidth(0);
     296            1 :     const headerHeight = columnHeader === TableColumnHeader.WithColumnPlots
     297            0 :         ? COLUMN_HEADER_HEIGHT + plotsHeight
     298            1 :         : COLUMN_HEADER_HEIGHT;
     299            1 :     const scrollbarHeight = useScrollbarHeight();
     300            1 :     const needsHorizontalScroll = totalColumnsWidth > gridContainerWidth;
     301            1 :     const horizontalScrollbarHeight = needsHorizontalScroll ? scrollbarHeight : 0;
     302            1 :     const gridContainerHeight = props.fillHeight ? (gridContainerSize?.height ?? 0) : computeGridHeight(
     303            1 :         dataTable.numRows,
     304            1 :         dataRowCount,
     305            1 :         headerHeight,
     306            1 :         horizontalScrollbarHeight,
     307            1 :         props.fitHeight ?? false,
     308            1 :         props.maxHeight,
     309            1 :         gridContainerSize?.height ?? 0,
     310            1 :         props.maxRows,
     311            1 :     );
     312            1 :     const firstRenderedColumn = gridLayout.columnCount > 1
     313            1 :         ? Math.max(1, Math.min(renderedCells.columnStart, gridLayout.columnCount - 1))
     314            0 :         : 1;
     315            1 :     const lastRenderedColumn = gridLayout.columnCount > 1
     316            1 :         ? Math.max(firstRenderedColumn, Math.min(renderedCells.columnStop, gridLayout.columnCount - 1))
     317            0 :         : 0;
     318            1 :     const renderedColumnCount = Math.max(0, lastRenderedColumn - firstRenderedColumn + 1);
     319            1 :     const leftHeaderSpacerWidth = gridLayout.columnCount > 1
     320            1 :         ? gridLayout.columnXOffsets[firstRenderedColumn] - firstColumnWidth
     321            0 :         : 0;
     322            1 :     const rightHeaderSpacerWidth = lastRenderedColumn > 0
     323            1 :         ? totalColumnsWidth - gridLayout.columnXOffsets[lastRenderedColumn + 1]
     324            0 :         : 0;
     325              : 
     326              : 
     327              :     // Keep the header cell props stable so that the memoized header cells skip unrelated renders
     328            1 :     const nameCellProps = React.useMemo(() => ({
     329            1 :         table: computationState.dataTable,
     330            1 :         gridLayout,
     331            1 :         dataFrame: computationState.dataFrame,
     332            1 :         rightmostVisibleColumn: gridLayout.columnCount - 1,
     333            1 :         getSortDirection,
     334            1 :         onOrderByColumn: orderByColumn,
     335            1 :         onShowTable: props.onShowTable,
     336            1 :     }), [computationState.dataTable, gridLayout, computationState.dataFrame, getSortDirection, orderByColumn, props.onShowTable]);
     337            1 :     const selectionEpoch = computationState.filterTable?.version ?? computationState.version;
     338            1 :     const dataSearchRequestId = computationState.dataSearchTable?.requestId ?? null;
     339            1 :     const plotCellProps = React.useMemo(() => ({
     340            1 :         gridLayout,
     341            1 :         columnGroups: computationState.columnGroups,
     342            1 :         columnAggregations: computationState.columnAggregates,
     343            1 :         columnAggregationTasks: computationState.tasks.columnAggregationTasks,
     344            1 :         filteredColumnAggregations: computationState.filteredColumnAggregates,
     345            1 :         filteredColumnAggregationTasks: computationState.tasks.filteredColumnAggregationTasks,
     346            1 :         filteredColumnAggregationOutdated: computationState.filteredColumnAggregatesOutdated,
     347            1 :         tableAggregation: computationState.tableAggregation,
     348            1 :         selectionEpoch,
     349            1 :         dataSearchRequestId,
     350            1 :         crossFilters,
     351            1 :         isVisible: true,
     352            1 :         rightmostVisibleColumn: gridLayout.columnCount - 1,
     353            1 :         onRequestFilteredColumnAggregation: requestFilteredColumnAggregation,
     354            1 :         onHistogramFilter: histogramFilter,
     355            1 :         onBrushingChange: NOOP_BRUSHING,
     356            1 :         onMostFrequentValueFilter: mostFrequentValueFilter,
     357            1 :     }), [
     358            1 :         gridLayout,
     359            1 :         computationState.columnGroups,
     360            1 :         computationState.columnAggregates,
     361            1 :         computationState.tasks.columnAggregationTasks,
     362            1 :         computationState.filteredColumnAggregates,
     363            1 :         computationState.tasks.filteredColumnAggregationTasks,
     364            1 :         computationState.filteredColumnAggregatesOutdated,
     365            1 :         computationState.tableAggregation,
     366            1 :         selectionEpoch,
     367            1 :         dataSearchRequestId,
     368            1 :         crossFilters,
     369            1 :         requestFilteredColumnAggregation,
     370            1 :         histogramFilter,
     371            1 :         mostFrequentValueFilter,
     372            1 :     ]);
     373              : 
     374              :     // Create containers for sticky header and sticky column
     375            1 :     const portalContainers = useDataTablePortalContainers(gridApi);
     376              : 
     377              :     // Drag the right border of any cell to resize its column
     378            1 :     const columnResize = useDataTableColumnResize(gridApi, gridLayout, dataTable, setResizedColumnWidths);
     379              : 
     380              :     // Total height of data content (for sticky column sizing)
     381            1 :     const totalDataHeight = dataRowCount * ROW_HEIGHT;
     382              : 
     383            1 :     return (
     384              :         // The grid can be rendered within the session feed's Electron drag region. Opt out so
     385              :         // native scrollbar thumbs receive pointer drags instead of moving the application window.
     386            1 :         <div
     387            1 :             className={classNames(styles.root, props.className, {
     388            1 :                 [styles.root_compact]: props.compact,
     389            1 :             })}
     390            1 :             data-electron-drag-region="false"
     391            1 :             style={{
     392            1 :                 ...(props.cellBackground ? { '--data_table_bg': props.cellBackground } : {}),
     393            1 :                 ...(props.fitHeight ? { height: gridContainerHeight } : {}),
     394            1 :             } as React.CSSProperties}
     395              :         >
     396            1 :             <div className={styles.grid_container} ref={gridContainerElement} {...columnResize}>
     397            1 :                 <Grid
     398            1 :                     gridRef={setGridApi}
     399            1 :                     style={{
     400            1 :                         width: gridContainerWidth,
     401            1 :                         height: gridContainerHeight,
     402            1 :                         overflowY: props.maxRows != null ? 'hidden' : undefined,
     403            1 :                     }}
     404            1 :                     columnCount={gridLayout.columnCount}
     405            1 :                     columnWidth={getColumnWidth}
     406            1 :                     rowCount={dataRowCount}
     407            1 :                     rowHeight={ROW_HEIGHT}
     408            1 :                     onCellsRendered={onCellsRendered}
     409            1 :                     overscanCount={GRID_OVERSCAN_COUNT}
     410            1 :                     cellComponent={DataCell}
     411            1 :                     cellProps={gridData}
     412            1 :                     className={styles.data_grid}
     413            1 :                 />
     414            1 :                 <DataTableStickyHeaders
     415            1 :                     container={portalContainers?.header ?? null}
     416            1 :                     totalWidth={totalColumnsWidth}
     417            1 :                     headerHeight={headerHeight}
     418            1 :                     firstColumnWidth={firstColumnWidth}
     419            1 :                     leftSpacerWidth={leftHeaderSpacerWidth}
     420            1 :                     rightSpacerWidth={rightHeaderSpacerWidth}
     421            1 :                     firstRenderedColumn={firstRenderedColumn}
     422            1 :                     renderedColumnCount={renderedColumnCount}
     423            1 :                     getColumnWidth={getColumnWidth}
     424            1 :                     columnHeader={columnHeader}
     425            1 :                     plotsHeight={plotsHeight}
     426            1 :                     plotsCollapsed={plotsCollapsed}
     427            1 :                     onTogglePlots={togglePlots}
     428            1 :                     nameCellProps={nameCellProps}
     429            1 :                     plotCellProps={plotCellProps}
     430            1 :                 />
     431            1 :                 <DataTableStickyColumn
     432            1 :                     container={portalContainers?.data ?? null}
     433            1 :                     firstColumnWidth={firstColumnWidth}
     434            1 :                     totalDataHeight={totalDataHeight}
     435            1 :                     startRow={Math.max(0, renderedCells.rowStart)}
     436            1 :                     stopRow={Math.min(dataRowCount - 1, renderedCells.rowStop)}
     437            1 :                     gridData={gridData}
     438            1 :                 />
     439            1 :             </div>
     440            1 :             <CellDetailOverlay
     441            1 :                 isOpen={cellDetail != null}
     442            1 :                 onClose={() => setCellDetail(null)}
     443            1 :                 formattedValue={cellDetail?.formattedValue ?? null}
     444            1 :                 structuredValue={cellDetail?.structuredValue ?? null}
     445            1 :                 columnName={cellDetail?.columnName ?? null}
     446            1 :                 dataRow={cellDetail?.visibleRow ?? 0}
     447            1 :                 maxRow={dataRowCount - 1}
     448            1 :                 onNavigate={onNavigateCell}
     449            1 :             />
     450            1 :         </div>
     451              :     );
     452            1 : });
        

Generated by: LCOV version 2.0-1