You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2022/08/22 13:18:19 UTC
[superset] 30/36: chore: refactor FilterableTable to functional component (#21136)
This is an automated email from the ASF dual-hosted git repository.
diegopucci pushed a commit to branch chore/drill-to-detail-modal-tests
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2a9f910129b4c9d57043f327ed20e5f76154afbd
Author: EugeneTorap <ev...@gmail.com>
AuthorDate: Mon Aug 22 09:19:36 2022 +0300
chore: refactor FilterableTable to functional component (#21136)
---
.../src/components/FilterableTable/index.tsx | 462 +++++++++------------
1 file changed, 192 insertions(+), 270 deletions(-)
diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx
index 621565b154..2a56bd7dca 100644
--- a/superset-frontend/src/components/FilterableTable/index.tsx
+++ b/superset-frontend/src/components/FilterableTable/index.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import JSONbig from 'json-bigint';
-import React, { PureComponent } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import JSONTree from 'react-json-tree';
import {
AutoSizer,
@@ -33,8 +33,7 @@ import {
getMultipleTextDimensions,
t,
styled,
- SupersetTheme,
- withTheme,
+ useTheme,
} from '@superset-ui/core';
import Button from '../Button';
import CopyToClipboard from '../CopyToClipboard';
@@ -188,98 +187,65 @@ export interface FilterableTableProps {
orderedColumnKeys: string[];
data: Record<string, unknown>[];
height: number;
- filterText: string;
- headerHeight: number;
- overscanColumnCount: number;
- overscanRowCount: number;
- rowHeight: number;
- striped: boolean;
- expandedColumns: string[];
- theme: SupersetTheme;
+ filterText?: string;
+ headerHeight?: number;
+ overscanColumnCount?: number;
+ overscanRowCount?: number;
+ rowHeight?: number;
+ striped?: boolean;
+ expandedColumns?: string[];
}
-interface FilterableTableState {
- sortBy?: string;
- sortDirection?: SortDirectionType;
- fitted: boolean;
- displayedList: Datum[];
-}
-
-class FilterableTable extends PureComponent<
- FilterableTableProps,
- FilterableTableState
-> {
- static defaultProps = {
- filterText: '',
- headerHeight: 32,
- overscanColumnCount: 10,
- overscanRowCount: 10,
- rowHeight: 32,
- striped: true,
- expandedColumns: [],
- };
-
- list: Datum[];
-
- complexColumns: Record<string, boolean>;
-
- widthsForColumnsByKey: Record<string, number>;
-
- totalTableWidth: number;
-
- totalTableHeight: number;
-
- container: React.RefObject<HTMLDivElement>;
-
- jsonTreeTheme: Record<string, string>;
-
- constructor(props: FilterableTableProps) {
- super(props);
- this.list = this.formatTableData(props.data);
- this.addJsonModal = this.addJsonModal.bind(this);
- this.getCellContent = this.getCellContent.bind(this);
- this.renderGridCell = this.renderGridCell.bind(this);
- this.renderGridCellHeader = this.renderGridCellHeader.bind(this);
- this.renderGrid = this.renderGrid.bind(this);
- this.renderTableCell = this.renderTableCell.bind(this);
- this.renderTableHeader = this.renderTableHeader.bind(this);
- this.sortResults = this.sortResults.bind(this);
- this.renderTable = this.renderTable.bind(this);
- this.rowClassName = this.rowClassName.bind(this);
- this.sort = this.sort.bind(this);
- this.getJsonTreeTheme = this.getJsonTreeTheme.bind(this);
+const FilterableTable = ({
+ orderedColumnKeys,
+ data,
+ height,
+ filterText = '',
+ headerHeight = 32,
+ overscanColumnCount = 10,
+ overscanRowCount = 10,
+ rowHeight = 32,
+ striped = true,
+ expandedColumns = [],
+}: FilterableTableProps) => {
+ const formatTableData = (data: Record<string, unknown>[]): Datum[] =>
+ data.map(row => {
+ const newRow = {};
+ Object.entries(row).forEach(([key, val]) => {
+ if (['string', 'number'].indexOf(typeof val) >= 0) {
+ newRow[key] = val;
+ } else {
+ newRow[key] = val === null ? null : JSONbig.stringify(val);
+ }
+ });
+ return newRow;
+ });
- // columns that have complex type and were expanded into sub columns
- this.complexColumns = props.orderedColumnKeys.reduce(
+ const [sortByState, setSortByState] = useState<string | undefined>(undefined);
+ const [sortDirectionState, setSortDirectionState] = useState<
+ SortDirectionType | undefined
+ >(undefined);
+ const [fitted, setFitted] = useState(false);
+ const [list] = useState<Datum[]>(() => formatTableData(data));
+ const [displayedList, setDisplayedList] = useState<Datum[]>(list);
+
+ // columns that have complex type and were expanded into sub columns
+ const [complexColumns] = useState<Record<string, boolean>>(
+ orderedColumnKeys.reduce(
(obj, key) => ({
...obj,
- [key]: props.expandedColumns.some(name => name.startsWith(`${key}.`)),
+ [key]: expandedColumns.some(name => name.startsWith(`${key}.`)),
}),
{},
- );
+ ),
+ );
- this.widthsForColumnsByKey = this.getWidthsForColumns();
- this.totalTableWidth = props.orderedColumnKeys
- .map(key => this.widthsForColumnsByKey[key])
- .reduce((curr, next) => curr + next);
- this.totalTableHeight = props.height;
+ const theme = useTheme();
+ const [jsonTreeTheme, setJsonTreeTheme] = useState<Record<string, string>>();
- this.state = {
- fitted: false,
- displayedList: [...this.list],
- };
-
- this.container = React.createRef();
- }
-
- componentDidMount() {
- this.fitTableToWidthIfNeeded();
- }
-
- getJsonTreeTheme() {
- if (!this.jsonTreeTheme) {
- const { theme } = this.props;
- this.jsonTreeTheme = {
+ const getJsonTreeTheme = () => {
+ if (!jsonTreeTheme) {
+ setJsonTreeTheme({
base00: theme.colors.grayscale.dark2,
base01: theme.colors.grayscale.dark1,
base02: theme.colors.grayscale.base,
@@ -296,22 +262,20 @@ class FilterableTable extends PureComponent<
base0D: theme.colors.primary.base,
base0E: theme.colors.primary.dark1,
base0F: theme.colors.error.dark1,
- };
+ });
}
- return this.jsonTreeTheme;
- }
+ return jsonTreeTheme;
+ };
- getDatum(list: Datum[], index: number) {
- return list[index % list.length];
- }
+ const getDatum = (list: Datum[], index: number) => list[index % list.length];
- getWidthsForColumns() {
+ const getWidthsForColumns = () => {
const PADDING = 50; // accounts for cell padding and width of sorting icon
const widthsByColumnKey = {};
const cellContent = ([] as string[]).concat(
- ...this.props.orderedColumnKeys.map(key => {
- const cellContentList = this.list.map((data: Datum) =>
- this.getCellContent({ cellData: data[key], columnKey: key }),
+ ...orderedColumnKeys.map(key => {
+ const cellContentList = list.map((data: Datum) =>
+ getCellContent({ cellData: data[key], columnKey: key }),
);
cellContentList.push(key);
return cellContentList;
@@ -323,30 +287,26 @@ class FilterableTable extends PureComponent<
texts: cellContent,
}).map(dimension => dimension.width);
- this.props.orderedColumnKeys.forEach((key, index) => {
+ orderedColumnKeys.forEach((key, index) => {
// we can't use Math.max(...colWidths.slice(...)) here since the number
// of elements might be bigger than the number of allowed arguments in a
- // Javascript function
- const value = (widthsByColumnKey[key] =
+ // JavaScript function
+ widthsByColumnKey[key] =
colWidths
- .slice(
- index * (this.list.length + 1),
- (index + 1) * (this.list.length + 1),
- )
- .reduce((a, b) => Math.max(a, b)) + PADDING);
- widthsByColumnKey[key] = value;
+ .slice(index * (list.length + 1), (index + 1) * (list.length + 1))
+ .reduce((a, b) => Math.max(a, b)) + PADDING;
});
return widthsByColumnKey;
- }
+ };
- getCellContent({
+ const getCellContent = ({
cellData,
columnKey,
}: {
cellData: CellDataType;
columnKey: string;
- }) {
+ }) => {
if (cellData === null) {
return 'NULL';
}
@@ -360,24 +320,35 @@ class FilterableTable extends PureComponent<
} else {
truncated = '';
}
- return this.complexColumns[columnKey] ? truncated : content;
- }
+ return complexColumns[columnKey] ? truncated : content;
+ };
- formatTableData(data: Record<string, unknown>[]): Datum[] {
- return data.map(row => {
- const newRow = {};
- Object.entries(row).forEach(([key, val]) => {
- if (['string', 'number'].indexOf(typeof val) >= 0) {
- newRow[key] = val;
- } else {
- newRow[key] = val === null ? null : JSONbig.stringify(val);
- }
- });
- return newRow;
- });
- }
+ const [widthsForColumnsByKey] = useState<Record<string, number>>(() =>
+ getWidthsForColumns(),
+ );
+
+ const totalTableWidth = useRef(
+ orderedColumnKeys
+ .map(key => widthsForColumnsByKey[key])
+ .reduce((curr, next) => curr + next),
+ );
+ const totalTableHeight = useRef(height);
+ const container = useRef<HTMLDivElement>(null);
+
+ const fitTableToWidthIfNeeded = () => {
+ const containerWidth = container.current?.clientWidth ?? 0;
+ if (totalTableWidth.current < containerWidth) {
+ // fit table width if content doesn't fill the width of the container
+ totalTableWidth.current = containerWidth;
+ }
+ setFitted(true);
+ };
- hasMatch(text: string, row: Datum) {
+ useEffect(() => {
+ fitTableToWidthIfNeeded();
+ }, []);
+
+ const hasMatch = (text: string, row: Datum) => {
const values: string[] = [];
Object.keys(row).forEach(key => {
if (row.hasOwnProperty(key)) {
@@ -394,82 +365,60 @@ class FilterableTable extends PureComponent<
});
const lowerCaseText = text.toLowerCase();
return values.some(v => v.includes(lowerCaseText));
- }
+ };
- rowClassName({ index }: { index: number }) {
+ const rowClassName = ({ index }: { index: number }) => {
let className = '';
- if (this.props.striped) {
+ if (striped) {
className = index % 2 === 0 ? 'even-row' : 'odd-row';
}
return className;
- }
+ };
- sort({
+ const sort = ({
sortBy,
sortDirection,
}: {
sortBy: string;
sortDirection: SortDirectionType;
- }) {
- let updatedState: FilterableTableState;
-
+ }) => {
const shouldClearSort =
- this.state.sortDirection === SortDirection.DESC &&
- this.state.sortBy === sortBy;
+ sortDirectionState === SortDirection.DESC && sortByState === sortBy;
if (shouldClearSort) {
- updatedState = {
- ...this.state,
- sortBy: undefined,
- sortDirection: undefined,
- displayedList: [...this.list],
- };
+ setSortByState(undefined);
+ setSortDirectionState(undefined);
+ setDisplayedList([...list]);
} else {
- updatedState = {
- ...this.state,
- sortBy,
- sortDirection,
- displayedList: [...this.list].sort(
- this.sortResults(sortBy, sortDirection === SortDirection.DESC),
+ setSortByState(sortBy);
+ setSortDirectionState(sortDirection);
+ setDisplayedList(
+ [...list].sort(
+ sortResults(sortBy, sortDirection === SortDirection.DESC),
),
- };
- }
-
- this.setState(updatedState);
- }
-
- fitTableToWidthIfNeeded() {
- const containerWidth = this.container.current?.clientWidth ?? 0;
- if (this.totalTableWidth < containerWidth) {
- // fit table width if content doesn't fill the width of the container
- this.totalTableWidth = containerWidth;
+ );
}
- this.setState({ fitted: true });
- }
+ };
- addJsonModal(
+ const addJsonModal = (
node: React.ReactNode,
jsonObject: Record<string, unknown> | unknown[],
jsonString: CellDataType,
- ) {
- return (
- <ModalTrigger
- modalBody={
- <JSONTree data={jsonObject} theme={this.getJsonTreeTheme()} />
- }
- modalFooter={
- <Button>
- <CopyToClipboard shouldShowText={false} text={jsonString} />
- </Button>
- }
- modalTitle={t('Cell content')}
- triggerNode={node}
- />
- );
- }
+ ) => (
+ <ModalTrigger
+ modalBody={<JSONTree data={jsonObject} theme={getJsonTreeTheme()} />}
+ modalFooter={
+ <Button>
+ <CopyToClipboard shouldShowText={false} text={jsonString} />
+ </Button>
+ }
+ modalTitle={t('Cell content')}
+ triggerNode={node}
+ />
+ );
// Parse any numbers from strings so they'll sort correctly
- parseNumberFromString = (value: string | number | null) => {
+ const parseNumberFromString = (value: string | number | null) => {
if (typeof value === 'string') {
if (ONLY_NUMBER_REGEX.test(value)) {
return parseFloat(value);
@@ -479,10 +428,10 @@ class FilterableTable extends PureComponent<
return value;
};
- sortResults(sortBy: string, descending: boolean) {
- return (a: Datum, b: Datum) => {
- const aValue = this.parseNumberFromString(a[sortBy]);
- const bValue = this.parseNumberFromString(b[sortBy]);
+ const sortResults =
+ (sortBy: string, descending: boolean) => (a: Datum, b: Datum) => {
+ const aValue = parseNumberFromString(a[sortBy]);
+ const bValue = parseNumberFromString(b[sortBy]);
// equal items sort equally
if (aValue === bValue) {
@@ -502,20 +451,18 @@ class FilterableTable extends PureComponent<
}
return aValue < bValue ? -1 : 1;
};
- }
- sortGrid = (label: string) => {
- this.sort({
+ const sortGrid = (label: string) => {
+ sort({
sortBy: label,
sortDirection:
- this.state.sortDirection === SortDirection.DESC ||
- this.state.sortBy !== label
+ sortDirectionState === SortDirection.DESC || sortByState !== label
? SortDirection.ASC
: SortDirection.DESC,
});
};
- renderTableHeader({
+ const renderTableHeader = ({
dataKey,
label,
sortBy,
@@ -525,9 +472,9 @@ class FilterableTable extends PureComponent<
label: string;
sortBy: string;
sortDirection: SortDirectionType;
- }) {
+ }) => {
const className =
- this.props.expandedColumns.indexOf(label) > -1
+ expandedColumns.indexOf(label) > -1
? 'header-style-disabled'
: 'header-style';
@@ -537,9 +484,9 @@ class FilterableTable extends PureComponent<
{sortBy === dataKey && <SortIndicator sortDirection={sortDirection} />}
</div>
);
- }
+ };
- renderGridCellHeader({
+ const renderGridCellHeader = ({
columnIndex,
key,
style,
@@ -547,10 +494,10 @@ class FilterableTable extends PureComponent<
columnIndex: number;
key: string;
style: React.CSSProperties;
- }) {
- const label = this.props.orderedColumnKeys[columnIndex];
+ }) => {
+ const label = orderedColumnKeys[columnIndex];
const className =
- this.props.expandedColumns.indexOf(label) > -1
+ expandedColumns.indexOf(label) > -1
? 'header-style-disabled'
: 'header-style';
return (
@@ -566,17 +513,17 @@ class FilterableTable extends PureComponent<
className={`${className} grid-cell grid-header-cell`}
role="columnheader"
tabIndex={columnIndex}
- onClick={() => this.sortGrid(label)}
+ onClick={() => sortGrid(label)}
>
{label}
- {this.state.sortBy === label && (
- <SortIndicator sortDirection={this.state.sortDirection} />
+ {sortByState === label && (
+ <SortIndicator sortDirection={sortDirectionState} />
)}
</div>
);
- }
+ };
- renderGridCell({
+ const renderGridCell = ({
columnIndex,
key,
rowIndex,
@@ -586,10 +533,10 @@ class FilterableTable extends PureComponent<
key: string;
rowIndex: number;
style: React.CSSProperties;
- }) {
- const columnKey = this.props.orderedColumnKeys[columnIndex];
- const cellData = this.state.displayedList[rowIndex][columnKey];
- const cellText = this.getCellContent({ cellData, columnKey });
+ }) => {
+ const columnKey = orderedColumnKeys[columnIndex];
+ const cellData = displayedList[rowIndex][columnKey];
+ const cellText = getCellContent({ cellData, columnKey });
const content =
cellData === null ? <i className="text-muted">{cellText}</i> : cellText;
const cellNode = (
@@ -602,7 +549,7 @@ class FilterableTable extends PureComponent<
? style.top - GRID_POSITION_ADJUSTMENT
: style.top,
}}
- className={`grid-cell ${this.rowClassName({ index: rowIndex })}`}
+ className={`grid-cell ${rowClassName({ index: rowIndex })}`}
>
<div css={{ width: 'inherit' }}>{content}</div>
</div>
@@ -610,33 +557,23 @@ class FilterableTable extends PureComponent<
const jsonObject = safeJsonObjectParse(cellData);
if (jsonObject) {
- return this.addJsonModal(cellNode, jsonObject, cellData);
+ return addJsonModal(cellNode, jsonObject, cellData);
}
return cellNode;
- }
-
- renderGrid() {
- const {
- orderedColumnKeys,
- overscanColumnCount,
- overscanRowCount,
- rowHeight,
- } = this.props;
+ };
- let { height } = this.props;
- let totalTableHeight = height;
+ const renderGrid = () => {
if (
- this.container.current &&
- this.totalTableWidth > this.container.current.clientWidth
+ container.current &&
+ totalTableWidth.current > container.current.clientWidth
) {
// exclude the height of the horizontal scroll bar from the height of the table
// and the height of the table container if the content overflows
- height -= SCROLL_BAR_HEIGHT;
- totalTableHeight -= SCROLL_BAR_HEIGHT;
+ totalTableHeight.current -= SCROLL_BAR_HEIGHT;
}
const getColumnWidth = ({ index }: { index: number }) =>
- this.widthsForColumnsByKey[orderedColumnKeys[index]];
+ widthsForColumnsByKey[orderedColumnKeys[index]];
// fix height of filterable table
return (
@@ -648,7 +585,7 @@ class FilterableTable extends PureComponent<
{({ width }) => (
<div>
<Grid
- cellRenderer={this.renderGridCellHeader}
+ cellRenderer={renderGridCellHeader}
columnCount={orderedColumnKeys.length}
columnWidth={getColumnWidth}
height={rowHeight}
@@ -659,14 +596,14 @@ class FilterableTable extends PureComponent<
style={{ overflow: 'hidden' }}
/>
<Grid
- cellRenderer={this.renderGridCell}
+ cellRenderer={renderGridCell}
columnCount={orderedColumnKeys.length}
columnWidth={getColumnWidth}
- height={totalTableHeight - rowHeight}
+ height={totalTableHeight.current - rowHeight}
onScroll={onScroll}
overscanColumnCount={overscanColumnCount}
overscanRowCount={overscanRowCount}
- rowCount={this.list.length}
+ rowCount={list.length}
rowHeight={rowHeight}
width={width}
/>
@@ -678,86 +615,73 @@ class FilterableTable extends PureComponent<
</ScrollSync>
</StyledFilterableTable>
);
- }
+ };
- renderTableCell({
+ const renderTableCell = ({
cellData,
columnKey,
}: {
cellData: CellDataType;
columnKey: string;
- }) {
- const cellNode = this.getCellContent({ cellData, columnKey });
+ }) => {
+ const cellNode = getCellContent({ cellData, columnKey });
const content =
cellData === null ? <i className="text-muted">{cellNode}</i> : cellNode;
const jsonObject = safeJsonObjectParse(cellData);
if (jsonObject) {
- return this.addJsonModal(cellNode, jsonObject, cellData);
+ return addJsonModal(cellNode, jsonObject, cellData);
}
return content;
- }
+ };
- renderTable() {
- const { sortBy, sortDirection } = this.state;
- const {
- filterText,
- headerHeight,
- orderedColumnKeys,
- overscanRowCount,
- rowHeight,
- } = this.props;
-
- let sortedAndFilteredList = this.state.displayedList;
+ const renderTable = () => {
+ let sortedAndFilteredList = displayedList;
// filter list
if (filterText) {
sortedAndFilteredList = sortedAndFilteredList.filter((row: Datum) =>
- this.hasMatch(filterText, row),
+ hasMatch(filterText, row),
);
}
- let { height } = this.props;
- let totalTableHeight = height;
if (
- this.container.current &&
- this.totalTableWidth > this.container.current.clientWidth
+ container.current &&
+ totalTableWidth.current > container.current.clientWidth
) {
// exclude the height of the horizontal scroll bar from the height of the table
// and the height of the table container if the content overflows
- height -= SCROLL_BAR_HEIGHT;
- totalTableHeight -= SCROLL_BAR_HEIGHT;
+ totalTableHeight.current -= SCROLL_BAR_HEIGHT;
}
const rowGetter = ({ index }: { index: number }) =>
- this.getDatum(sortedAndFilteredList, index);
+ getDatum(sortedAndFilteredList, index);
return (
<StyledFilterableTable
className="filterable-table-container"
- ref={this.container}
+ ref={container}
>
- {this.state.fitted && (
+ {fitted && (
<Table
- ref="Table"
headerHeight={headerHeight}
- height={totalTableHeight}
+ height={totalTableHeight.current}
overscanRowCount={overscanRowCount}
- rowClassName={this.rowClassName}
+ rowClassName={rowClassName}
rowHeight={rowHeight}
rowGetter={rowGetter}
rowCount={sortedAndFilteredList.length}
- sort={this.sort}
- sortBy={sortBy}
- sortDirection={sortDirection}
- width={this.totalTableWidth}
+ sort={sort}
+ sortBy={sortByState}
+ sortDirection={sortDirectionState}
+ width={totalTableWidth.current}
>
{orderedColumnKeys.map(columnKey => (
<Column
cellRenderer={({ cellData }) =>
- this.renderTableCell({ cellData, columnKey })
+ renderTableCell({ cellData, columnKey })
}
dataKey={columnKey}
disableSort={false}
- headerRenderer={this.renderTableHeader}
- width={this.widthsForColumnsByKey[columnKey]}
+ headerRenderer={renderTableHeader}
+ width={widthsForColumnsByKey[columnKey]}
label={columnKey}
key={columnKey}
/>
@@ -766,14 +690,12 @@ class FilterableTable extends PureComponent<
)}
</StyledFilterableTable>
);
- }
+ };
- render() {
- if (this.props.orderedColumnKeys.length > MAX_COLUMNS_FOR_TABLE) {
- return this.renderGrid();
- }
- return this.renderTable();
+ if (orderedColumnKeys.length > MAX_COLUMNS_FOR_TABLE) {
+ return renderGrid();
}
-}
+ return renderTable();
+};
-export default withTheme(FilterableTable);
+export default FilterableTable;