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 : }
|