chore: type FilterableTable (#10073)

This commit is contained in:
Erik Ritter 2020-06-18 21:57:11 -07:00 committed by GitHub
parent a6390afb89
commit 2e76fbb7e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 180 additions and 63 deletions

View File

@ -9753,6 +9753,14 @@
} }
} }
}, },
"@types/cheerio": {
"version": "0.22.18",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.18.tgz",
"integrity": "sha512-Fq7R3fINAPSdUEhOyjG4iVxgHrOnqDJbY0/BUuiN0pvD/rfmZWekVZnv+vcs8TtpA2XF50uv50LaE4EnpEL/Hw==",
"requires": {
"@types/node": "*"
}
},
"@types/classnames": { "@types/classnames": {
"version": "2.2.9", "version": "2.2.9",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz",
@ -9852,6 +9860,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/enzyme": {
"version": "3.10.5",
"resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz",
"integrity": "sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA==",
"requires": {
"@types/cheerio": "*",
"@types/react": "*"
}
},
"@types/eslint-visitor-keys": { "@types/eslint-visitor-keys": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@ -10123,6 +10140,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-virtualized": {
"version": "9.21.10",
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.10.tgz",
"integrity": "sha512-f5Ti3A7gGdLkPPFNHTrvKblpsPNBiQoSorOEOD+JPx72g/Ng2lOt4MYfhvQFQNgyIrAro+Z643jbcKafsMW2ag==",
"requires": {
"@types/prop-types": "*",
"@types/react": "*"
}
},
"@types/react-window": { "@types/react-window": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz",

View File

@ -101,9 +101,11 @@
"@superset-ui/translation": "^0.13.27", "@superset-ui/translation": "^0.13.27",
"@superset-ui/validator": "^0.13.27", "@superset-ui/validator": "^0.13.27",
"@types/classnames": "^2.2.9", "@types/classnames": "^2.2.9",
"@types/enzyme": "^3.10.5",
"@types/react-bootstrap": "^0.32.21", "@types/react-bootstrap": "^0.32.21",
"@types/react-json-tree": "^0.6.11", "@types/react-json-tree": "^0.6.11",
"@types/react-select": "^3.0.12", "@types/react-select": "^3.0.12",
"@types/react-virtualized": "^9.21.10",
"@types/react-window": "^1.8.2", "@types/react-window": "^1.8.2",
"@types/redux-localstorage": "^1.0.8", "@types/redux-localstorage": "^1.0.8",
"@types/rison": "0.0.6", "@types/rison": "0.0.6",

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount, ReactWrapper } from 'enzyme';
import FilterableTable, { import FilterableTable, {
MAX_COLUMNS_FOR_TABLE, MAX_COLUMNS_FOR_TABLE,
} from 'src/components/FilterableTable/FilterableTable'; } from 'src/components/FilterableTable/FilterableTable';
@ -32,7 +32,7 @@ describe('FilterableTable', () => {
], ],
height: 500, height: 500,
}; };
let wrapper; let wrapper: ReactWrapper;
beforeEach(() => { beforeEach(() => {
wrapper = mount(<FilterableTable {...mockedProps} />); wrapper = mount(<FilterableTable {...mockedProps} />);
}); });
@ -53,11 +53,11 @@ describe('FilterableTable', () => {
(_, x) => `col_${x}`, (_, x) => `col_${x}`,
), ),
data: [ data: [
Object.assign( {
...Array.from(Array(wideTableColumns)).map((val, x) => ({ ...Array.from(Array(wideTableColumns)).map((val, x) => ({
[`col_${x}`]: x, [`col_${x}`]: x,
})), })),
), },
], ],
height: 500, height: 500,
}; };

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { List } from 'immutable'; import { List } from 'immutable';
import PropTypes from 'prop-types'; // @ts-ignore
import JSONbig from 'json-bigint'; import JSONbig from 'json-bigint';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import JSONTree from 'react-json-tree'; import JSONTree from 'react-json-tree';
@ -28,6 +28,7 @@ import {
SortDirection, SortDirection,
SortIndicator, SortIndicator,
Table, Table,
SortDirectionType,
} from 'react-virtualized'; } from 'react-virtualized';
import { getMultipleTextDimensions } from '@superset-ui/dimension'; import { getMultipleTextDimensions } from '@superset-ui/dimension';
import { t } from '@superset-ui/translation'; import { t } from '@superset-ui/translation';
@ -37,7 +38,9 @@ import CopyToClipboard from '../CopyToClipboard';
import ModalTrigger from '../ModalTrigger'; import ModalTrigger from '../ModalTrigger';
import TooltipWrapper from '../TooltipWrapper'; import TooltipWrapper from '../TooltipWrapper';
function safeJsonObjectParse(data) { function safeJsonObjectParse(
data: unknown,
): null | unknown[] | Record<string, unknown> {
// First perform a cheap proxy to avoid calling JSON.parse on data that is clearly not a // First perform a cheap proxy to avoid calling JSON.parse on data that is clearly not a
// JSON object or array // JSON object or array
if ( if (
@ -85,31 +88,43 @@ const JSON_TREE_THEME = {
// when more than MAX_COLUMNS_FOR_TABLE are returned, switch from table to grid view // when more than MAX_COLUMNS_FOR_TABLE are returned, switch from table to grid view
export const MAX_COLUMNS_FOR_TABLE = 50; export const MAX_COLUMNS_FOR_TABLE = 50;
const propTypes = { type CellDataType = string | number | null;
orderedColumnKeys: PropTypes.array.isRequired, type Datum = Record<string, CellDataType>;
data: PropTypes.array.isRequired,
height: PropTypes.number.isRequired,
filterText: PropTypes.string,
headerHeight: PropTypes.number,
overscanColumnCount: PropTypes.number,
overscanRowCount: PropTypes.number,
rowHeight: PropTypes.number,
striped: PropTypes.bool,
expandedColumns: PropTypes.array,
};
const defaultProps = { interface FilterableTableProps {
filterText: '', orderedColumnKeys: string[];
headerHeight: 32, data: Record<string, unknown>[];
overscanColumnCount: 10, height: number;
overscanRowCount: 10, filterText: string;
rowHeight: 32, headerHeight: number;
striped: true, overscanColumnCount: number;
expandedColumns: [], overscanRowCount: number;
}; rowHeight: number;
striped: boolean;
expandedColumns: string[];
}
export default class FilterableTable extends PureComponent { interface FilterableTableState {
constructor(props) { sortBy?: string;
sortDirection: SortDirectionType;
fitted: boolean;
}
export default class FilterableTable extends PureComponent<
FilterableTableProps,
FilterableTableState
> {
static defaultProps = {
filterText: '',
headerHeight: 32,
overscanColumnCount: 10,
overscanRowCount: 10,
rowHeight: 32,
striped: true,
expandedColumns: [],
};
constructor(props: FilterableTableProps) {
super(props); super(props);
this.list = List(this.formatTableData(props.data)); this.list = List(this.formatTableData(props.data));
this.addJsonModal = this.addJsonModal.bind(this); this.addJsonModal = this.addJsonModal.bind(this);
@ -140,7 +155,6 @@ export default class FilterableTable extends PureComponent {
this.totalTableHeight = props.height; this.totalTableHeight = props.height;
this.state = { this.state = {
sortBy: null,
sortDirection: SortDirection.ASC, sortDirection: SortDirection.ASC,
fitted: false, fitted: false,
}; };
@ -152,7 +166,7 @@ export default class FilterableTable extends PureComponent {
this.fitTableToWidthIfNeeded(); this.fitTableToWidthIfNeeded();
} }
getDatum(list, index) { getDatum(list: List<Datum>, index: number) {
return list.get(index % list.size); return list.get(index % list.size);
} }
@ -162,9 +176,10 @@ export default class FilterableTable extends PureComponent {
const cellContent = [].concat( const cellContent = [].concat(
...this.props.orderedColumnKeys.map(key => ...this.props.orderedColumnKeys.map(key =>
this.list this.list
.map(data => .map((data: Datum) =>
this.getCellContent({ cellData: data[key], columnKey: key }), this.getCellContent({ cellData: data[key], columnKey: key }),
) )
// @ts-ignore
.push(key) .push(key)
.toJS(), .toJS(),
), ),
@ -191,7 +206,13 @@ export default class FilterableTable extends PureComponent {
return widthsByColumnKey; return widthsByColumnKey;
} }
getCellContent({ cellData, columnKey }) { getCellContent({
cellData,
columnKey,
}: {
cellData: CellDataType;
columnKey: string;
}) {
if (cellData === null) { if (cellData === null) {
return <i className="text-muted">NULL</i>; return <i className="text-muted">NULL</i>;
} }
@ -208,7 +229,14 @@ export default class FilterableTable extends PureComponent {
return this.complexColumns[columnKey] ? truncated : content; return this.complexColumns[columnKey] ? truncated : content;
} }
formatTableData(data) { list: List<Datum>;
complexColumns: Record<string, boolean>;
widthsForColumnsByKey: Record<string, number>;
totalTableWidth: number;
totalTableHeight: number;
container: React.RefObject<HTMLDivElement>;
formatTableData(data: Record<string, unknown>[]): Datum[] {
const formattedData = data.map(row => { const formattedData = data.map(row => {
const newRow = {}; const newRow = {};
for (const k in row) { for (const k in row) {
@ -224,7 +252,7 @@ export default class FilterableTable extends PureComponent {
return formattedData; return formattedData;
} }
hasMatch(text, row) { hasMatch(text: string, row: Datum) {
const values = []; const values = [];
for (const key in row) { for (const key in row) {
if (row.hasOwnProperty(key)) { if (row.hasOwnProperty(key)) {
@ -243,7 +271,7 @@ export default class FilterableTable extends PureComponent {
return values.some(v => v.includes(lowerCaseText)); return values.some(v => v.includes(lowerCaseText));
} }
rowClassName({ index }) { rowClassName({ index }: { index: number }) {
let className = ''; let className = '';
if (this.props.striped) { if (this.props.striped) {
className = index % 2 === 0 ? 'even-row' : 'odd-row'; className = index % 2 === 0 ? 'even-row' : 'odd-row';
@ -251,12 +279,18 @@ export default class FilterableTable extends PureComponent {
return className; return className;
} }
sort({ sortBy, sortDirection }) { sort({
sortBy,
sortDirection,
}: {
sortBy: string;
sortDirection: SortDirectionType;
}) {
this.setState({ sortBy, sortDirection }); this.setState({ sortBy, sortDirection });
} }
fitTableToWidthIfNeeded() { fitTableToWidthIfNeeded() {
const containerWidth = this.container.clientWidth; const containerWidth = this.container.current!.clientWidth;
if (this.totalTableWidth < containerWidth) { if (this.totalTableWidth < containerWidth) {
// fit table width if content doesn't fill the width of the container // fit table width if content doesn't fill the width of the container
this.totalTableWidth = containerWidth; this.totalTableWidth = containerWidth;
@ -264,7 +298,11 @@ export default class FilterableTable extends PureComponent {
this.setState({ fitted: true }); this.setState({ fitted: true });
} }
addJsonModal(node, jsonObject, jsonString) { addJsonModal(
node: React.ReactNode,
jsonObject: Record<string, unknown> | unknown[],
jsonString: CellDataType,
) {
return ( return (
<ModalTrigger <ModalTrigger
modalBody={<JSONTree data={jsonObject} theme={JSON_TREE_THEME} />} modalBody={<JSONTree data={jsonObject} theme={JSON_TREE_THEME} />}
@ -279,24 +317,36 @@ export default class FilterableTable extends PureComponent {
); );
} }
sortResults(sortBy, descending) { sortResults(sortBy: string, descending: boolean) {
return (a, b) => { return (a: Datum, b: Datum) => {
if (a[sortBy] === b[sortBy]) { const aValue = a[sortBy];
const bValue = b[sortBy];
if (aValue === bValue) {
// equal items sort equally // equal items sort equally
return 0; return 0;
} else if (a[sortBy] === null) { } else if (aValue === null) {
// nulls sort after anything else // nulls sort after anything else
return 1; return 1;
} else if (b[sortBy] === null) { } else if (bValue === null) {
return -1; return -1;
} else if (descending) { } else if (descending) {
return a[sortBy] < b[sortBy] ? 1 : -1; return aValue < bValue ? 1 : -1;
} }
return a[sortBy] < b[sortBy] ? -1 : 1; return aValue < bValue ? -1 : 1;
}; };
} }
renderTableHeader({ dataKey, label, sortBy, sortDirection }) { renderTableHeader({
dataKey,
label,
sortBy,
sortDirection,
}: {
dataKey: string;
label: string;
sortBy: string;
sortDirection: SortDirectionType;
}) {
const className = const className =
this.props.expandedColumns.indexOf(label) > -1 this.props.expandedColumns.indexOf(label) > -1
? 'header-style-disabled' ? 'header-style-disabled'
@ -313,7 +363,15 @@ export default class FilterableTable extends PureComponent {
); );
} }
renderGridCellHeader({ columnIndex, key, style }) { renderGridCellHeader({
columnIndex,
key,
style,
}: {
columnIndex: number;
key: string;
style: React.CSSProperties;
}) {
const label = this.props.orderedColumnKeys[columnIndex]; const label = this.props.orderedColumnKeys[columnIndex];
const className = const className =
this.props.expandedColumns.indexOf(label) > -1 this.props.expandedColumns.indexOf(label) > -1
@ -322,7 +380,13 @@ export default class FilterableTable extends PureComponent {
return ( return (
<TooltipWrapper key={key} label="header" tooltip={label}> <TooltipWrapper key={key} label="header" tooltip={label}>
<div <div
style={{ ...style, top: style.top - GRID_POSITION_ADJUSTMENT }} style={{
...style,
top:
typeof style.top === 'number'
? style.top - GRID_POSITION_ADJUSTMENT
: style.top,
}}
className={`${className} grid-cell grid-header-cell`} className={`${className} grid-cell grid-header-cell`}
> >
{label} {label}
@ -331,14 +395,30 @@ export default class FilterableTable extends PureComponent {
); );
} }
renderGridCell({ columnIndex, key, rowIndex, style }) { renderGridCell({
columnIndex,
key,
rowIndex,
style,
}: {
columnIndex: number;
key: string;
rowIndex: number;
style: React.CSSProperties;
}) {
const columnKey = this.props.orderedColumnKeys[columnIndex]; const columnKey = this.props.orderedColumnKeys[columnIndex];
const cellData = this.list.get(rowIndex)[columnKey]; const cellData = this.list.get(rowIndex)[columnKey];
const content = this.getCellContent({ cellData, columnKey }); const content = this.getCellContent({ cellData, columnKey });
const cellNode = ( const cellNode = (
<div <div
key={key} key={key}
style={{ ...style, top: style.top - GRID_POSITION_ADJUSTMENT }} style={{
...style,
top:
typeof style.top === 'number'
? style.top - GRID_POSITION_ADJUSTMENT
: style.top,
}}
className={`grid-cell ${this.rowClassName({ index: rowIndex })}`} className={`grid-cell ${this.rowClassName({ index: rowIndex })}`}
> >
{content} {content}
@ -362,14 +442,17 @@ export default class FilterableTable extends PureComponent {
let { height } = this.props; let { height } = this.props;
let totalTableHeight = height; let totalTableHeight = height;
if (this.container && this.totalTableWidth > this.container.clientWidth) { if (
this.container.current &&
this.totalTableWidth > this.container.current.clientWidth
) {
// exclude the height of the horizontal scroll bar from the height of the table // 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 // and the height of the table container if the content overflows
height -= SCROLL_BAR_HEIGHT; height -= SCROLL_BAR_HEIGHT;
totalTableHeight -= SCROLL_BAR_HEIGHT; totalTableHeight -= SCROLL_BAR_HEIGHT;
} }
const getColumnWidth = ({ index }) => const getColumnWidth = ({ index }: { index: number }) =>
this.widthsForColumnsByKey[orderedColumnKeys[index]]; this.widthsForColumnsByKey[orderedColumnKeys[index]];
// fix height of filterable table // fix height of filterable table
@ -413,7 +496,13 @@ export default class FilterableTable extends PureComponent {
); );
} }
renderTableCell({ cellData, columnKey }) { renderTableCell({
cellData,
columnKey,
}: {
cellData: CellDataType;
columnKey: string;
}) {
const cellNode = this.getCellContent({ cellData, columnKey }); const cellNode = this.getCellContent({ cellData, columnKey });
const jsonObject = safeJsonObjectParse(cellData); const jsonObject = safeJsonObjectParse(cellData);
if (jsonObject) { if (jsonObject) {
@ -432,30 +521,33 @@ export default class FilterableTable extends PureComponent {
rowHeight, rowHeight,
} = this.props; } = this.props;
let sortedAndFilteredList = this.list; let sortedAndFilteredList: List<Datum> = this.list;
// filter list // filter list
if (filterText) { if (filterText) {
sortedAndFilteredList = this.list.filter(row => sortedAndFilteredList = this.list.filter((row: Datum) =>
this.hasMatch(filterText, row), this.hasMatch(filterText, row),
); ) as List<Datum>;
} }
// sort list // sort list
if (sortBy) { if (sortBy) {
sortedAndFilteredList = sortedAndFilteredList.sort( sortedAndFilteredList = sortedAndFilteredList.sort(
this.sortResults(sortBy, sortDirection === SortDirection.DESC), this.sortResults(sortBy, sortDirection === SortDirection.DESC),
); ) as List<Datum>;
} }
let { height } = this.props; let { height } = this.props;
let totalTableHeight = height; let totalTableHeight = height;
if (this.container && this.totalTableWidth > this.container.clientWidth) { if (
this.container.current &&
this.totalTableWidth > this.container.current.clientWidth
) {
// exclude the height of the horizontal scroll bar from the height of the table // 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 // and the height of the table container if the content overflows
height -= SCROLL_BAR_HEIGHT; height -= SCROLL_BAR_HEIGHT;
totalTableHeight -= SCROLL_BAR_HEIGHT; totalTableHeight -= SCROLL_BAR_HEIGHT;
} }
const rowGetter = ({ index }) => const rowGetter = ({ index }: { index: number }) =>
this.getDatum(sortedAndFilteredList, index); this.getDatum(sortedAndFilteredList, index);
return ( return (
<div <div
@ -504,6 +596,3 @@ export default class FilterableTable extends PureComponent {
return this.renderTable(); return this.renderTable();
} }
} }
FilterableTable.propTypes = propTypes;
FilterableTable.defaultProps = defaultProps;