LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_result - data_table_cell_data.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 74.5 % 106 79
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 type { CellComponentProps } from 'react-window';
       6              : 
       7              : import { ArrowTableFormatter, isArrowListType } from '../../compute/arrow_formatter.js';
       8              : import { ColumnGroup } from '../../compute/computation_types.js';
       9              : import { DataTableLayout } from './data_table_layout.js';
      10              : import { Tag } from '../../ui/foundations/tag.js';
      11              : import { peekFormat } from './format_peek.js';
      12              : import { CellFocus, DataTableFocusStore, useCellFocus } from './data_table_focus.js';
      13              : 
      14              : /// ---------------------------------------------------------------------------
      15              : /// Data Cell
      16              : /// ---------------------------------------------------------------------------
      17              : 
      18              : /// Cells are at most a few hundred pixels wide but text values can be megabytes large.
      19              : /// Render only a prefix, laying out the full text is what makes large cells slow.
      20            1 : const MAX_CELL_TEXT_LENGTH = 256;
      21              : 
      22            1 : function truncateCellText(text: string): string {
      23            1 :     return text.length > MAX_CELL_TEXT_LENGTH ? `${text.slice(0, MAX_CELL_TEXT_LENGTH)}…` : text;
      24            1 : }
      25              : 
      26              : export interface DataCellData {
      27              :     columnGroups: ColumnGroup[];
      28              :     visibleRowIndices: Int32Array | null;
      29              :     focus: DataTableFocusStore;
      30              :     gridLayout: DataTableLayout;
      31              :     hideRowHeader: boolean;
      32              :     table: arrow.Table;
      33              :     tableFormatter: ArrowTableFormatter;
      34              :     rightmostVisibleColumn: number;
      35              :     matchingRows: ReadonlyMap<number, number[]> | null;
      36              :     searchColumnIndexByGroup: ReadonlyMap<number, number>;
      37              :     onMouseEnter: (event: React.PointerEvent<HTMLDivElement>) => void;
      38              :     onMouseLeave: (event: React.PointerEvent<HTMLDivElement>) => void;
      39              :     onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
      40              : }
      41              : 
      42            1 : export function DataCell(props: CellComponentProps<DataCellData>): React.ReactElement | null {
      43            1 :     const inLayout = props.columnIndex < props.gridLayout.arrowFieldByColumnIndex.length;
      44            1 :     const isHiddenRowHeader = props.columnIndex === 0 && props.hideRowHeader;
      45            1 :     const fieldId = inLayout ? props.gridLayout.arrowFieldByColumnIndex[props.columnIndex] : -1;
      46              : 
      47              :     // Translate the row index through the visible row ids, if an indirection table is active
      48            1 :     const dataRow = props.visibleRowIndices != null
      49            0 :         ? props.visibleRowIndices[props.rowIndex]
      50            1 :         : props.rowIndex;
      51              : 
      52              :     // Subscribe to the hovered cell, only the previous and the next focused row re-render
      53            1 :     const focus = useCellFocus(props.focus, (inLayout && !isHiddenRowHeader) ? dataRow : -1, fieldId);
      54              : 
      55              :     // Format the value and peek into text values only when the value changes
      56            1 :     const formatted = (inLayout && !isHiddenRowHeader && props.tableFormatter)
      57            1 :         ? props.tableFormatter.getValue(dataRow, fieldId)
      58            1 :         : null;
      59            1 :     const peekText = inLayout && props.columnIndex !== 0 && props.gridLayout.isTextColumn[props.columnIndex] === 1;
      60            1 :     const peekedFormat = React.useMemo(() => peekText ? peekFormat(formatted) : null, [peekText, formatted]);
      61              : 
      62            1 :     if (!inLayout) {
      63            0 :         return <div style={props.style} />;
      64              :     }
      65            1 :     if (isHiddenRowHeader) {
      66            1 :         return null;
      67              :     }
      68              : 
      69              :     // Abort if no formatter is available
      70            1 :     if (!props.tableFormatter) {
      71            0 :         return (
      72            0 :             <div
      73            0 :                 className={styles.data_cell}
      74            0 :                 style={props.style}
      75            0 :                 data-table-col={fieldId}
      76            0 :                 data-table-row={dataRow}
      77            0 :                 data-visible-row={props.rowIndex}
      78            0 :                 onMouseEnter={props.onMouseEnter}
      79            0 :                 onMouseLeave={props.onMouseLeave}
      80            0 :             />
      81              :         );
      82              :     }
      83            1 :     const isRowFocused = focus !== CellFocus.None;
      84              : 
      85            1 :     if (props.columnIndex == 0) {
      86              :         // Row number column - inline class computation to avoid object allocation
      87            1 :         const className = isRowFocused
      88            0 :             ? `${styles.row_header_cell} ${styles.data_cell_focused_secondary}`
      89            1 :             : styles.row_header_cell;
      90            1 :         return (
      91            1 :             <div className={className} style={props.style}>
      92            1 :                 {formatted ?? ""}
      93            1 :             </div>
      94              :         );
      95            1 :     } else {
      96              :         // Compute class name inline to avoid object allocation in classNames()
      97            1 :         const isMetadata = props.gridLayout.isSystemColumn[props.columnIndex] === 1;
      98            1 :         const isNull = formatted == null;
      99            1 :         const displayText = isNull ? null : truncateCellText(formatted);
     100            1 :         const isRightmost = props.columnIndex === props.rightmostVisibleColumn;
     101            1 :         const searchColumnIndex = props.searchColumnIndexByGroup.get(
     102            1 :             props.gridLayout.columnGroupByColumnIndex[props.columnIndex],
     103            1 :         );
     104            1 :         const isSearchMatch = searchColumnIndex != null
     105            1 :             && !isMetadata
     106            1 :             && props.matchingRows?.get(dataRow + 1)?.includes(searchColumnIndex) === true;
     107              : 
     108              :         // Build class string directly - avoids object creation and iteration
     109            1 :         let className: string;
     110            1 :         if (isNull) {
     111            0 :             className = `${styles.data_cell} ${styles.data_cell_null}`;
     112              :         } else {
     113            1 :             className = styles.data_cell;
     114              :         }
     115            1 :         if (isRowFocused) {
     116            0 :             className += focus === CellFocus.Primary
     117            0 :                 ? ` ${styles.data_cell_focused_primary}`
     118            0 :                 : ` ${styles.data_cell_focused_secondary}`;
     119              :         }
     120            1 :         if (isMetadata) {
     121            0 :             className += ` ${styles.data_cell_metadata}`;
     122              :         }
     123            1 :         if (isRightmost) {
     124            1 :             className += ` ${styles.data_cell_rightmost}`;
     125              :         }
     126            1 :         if (isSearchMatch) {
     127            0 :             className += ` ${styles.data_cell_search_match}`;
     128              :         }
     129              : 
     130            1 :         const field = props.table.schema.fields[fieldId];
     131            1 :         const canOpenDetail = field.type.typeId === arrow.Type.Utf8
     132            1 :             || field.type.typeId === arrow.Type.LargeUtf8
     133            1 :             || isArrowListType(field.type)
     134            1 :             || field.type.typeId === arrow.Type.Struct;
     135            1 :         const hint = !isNull && isArrowListType(field.type)
     136            0 :             ? 'array'
     137            1 :             : (!isNull && field.type.typeId === arrow.Type.Struct
     138            0 :                 ? 'struct'
     139            1 :                 : peekedFormat);
     140              : 
     141            1 :         return (
     142            1 :             <div
     143            1 :                 className={className}
     144            1 :                 style={props.style}
     145            1 :                 role="gridcell"
     146            1 :                 aria-label={isSearchMatch
     147            0 :                     ? `${isNull ? 'NULL' : displayText}${hint == null ? '' : `, ${hint}`}, search match`
     148            1 :                     : undefined}
     149            1 :                 tabIndex={canOpenDetail ? 0 : -1}
     150            1 :                 data-table-col={fieldId}
     151            1 :                 data-table-row={dataRow}
     152            1 :                 data-visible-row={props.rowIndex}
     153            1 :                 onMouseEnter={props.onMouseEnter}
     154            1 :                 onMouseLeave={props.onMouseLeave}
     155            1 :                 onClick={props.onClick}
     156            1 :                 onKeyDown={event => {
     157            0 :                     if (canOpenDetail && (event.key === 'Enter' || event.key === ' ')) {
     158            0 :                         event.preventDefault();
     159            0 :                         event.currentTarget.click();
     160            0 :                     }
     161            0 :                 }}
     162              :             >
     163            1 :                 {hint != null ? (
     164            1 :                     <>
     165            1 :                         <span className={styles.data_cell_text}>{displayText}</span>
     166            1 :                         <Tag className={styles.format_bean}>{hint}</Tag>
     167            1 :                     </>
     168              :                 ) : (
     169            1 :                     isNull ? "NULL" : displayText
     170              :                 )}
     171            1 :             </div>
     172              :         );
     173            1 :     }
     174            1 : }
        

Generated by: LCOV version 2.0-1