From 2e76fbb7e56f4551d3733db82ce5a2e04ad6cd8d Mon Sep 17 00:00:00 2001 From: Erik Ritter Date: Thu, 18 Jun 2020 21:57:11 -0700 Subject: [PATCH] chore: type FilterableTable (#10073) --- superset-frontend/package-lock.json | 26 +++ superset-frontend/package.json | 2 + ...able_spec.jsx => FilterableTable_spec.tsx} | 8 +- ...ilterableTable.jsx => FilterableTable.tsx} | 207 +++++++++++++----- 4 files changed, 180 insertions(+), 63 deletions(-) rename superset-frontend/spec/javascripts/components/FilterableTable/{FilterableTable_spec.jsx => FilterableTable_spec.tsx} (96%) rename superset-frontend/src/components/FilterableTable/{FilterableTable.jsx => FilterableTable.tsx} (77%) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index f80cb17be8..cc3557ccbe 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -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": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz", @@ -9852,6 +9860,15 @@ "@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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -10123,6 +10140,15 @@ "@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": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 7d60f6ba22..921cb8150c 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -101,9 +101,11 @@ "@superset-ui/translation": "^0.13.27", "@superset-ui/validator": "^0.13.27", "@types/classnames": "^2.2.9", + "@types/enzyme": "^3.10.5", "@types/react-bootstrap": "^0.32.21", "@types/react-json-tree": "^0.6.11", "@types/react-select": "^3.0.12", + "@types/react-virtualized": "^9.21.10", "@types/react-window": "^1.8.2", "@types/redux-localstorage": "^1.0.8", "@types/rison": "0.0.6", diff --git a/superset-frontend/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx b/superset-frontend/spec/javascripts/components/FilterableTable/FilterableTable_spec.tsx similarity index 96% rename from superset-frontend/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx rename to superset-frontend/spec/javascripts/components/FilterableTable/FilterableTable_spec.tsx index 74cbf92023..2d2b451c16 100644 --- a/superset-frontend/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx +++ b/superset-frontend/spec/javascripts/components/FilterableTable/FilterableTable_spec.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import FilterableTable, { MAX_COLUMNS_FOR_TABLE, } from 'src/components/FilterableTable/FilterableTable'; @@ -32,7 +32,7 @@ describe('FilterableTable', () => { ], height: 500, }; - let wrapper; + let wrapper: ReactWrapper; beforeEach(() => { wrapper = mount(); }); @@ -53,11 +53,11 @@ describe('FilterableTable', () => { (_, x) => `col_${x}`, ), data: [ - Object.assign( + { ...Array.from(Array(wideTableColumns)).map((val, x) => ({ [`col_${x}`]: x, })), - ), + }, ], height: 500, }; diff --git a/superset-frontend/src/components/FilterableTable/FilterableTable.jsx b/superset-frontend/src/components/FilterableTable/FilterableTable.tsx similarity index 77% rename from superset-frontend/src/components/FilterableTable/FilterableTable.jsx rename to superset-frontend/src/components/FilterableTable/FilterableTable.tsx index 5b0e111106..236f25d87f 100644 --- a/superset-frontend/src/components/FilterableTable/FilterableTable.jsx +++ b/superset-frontend/src/components/FilterableTable/FilterableTable.tsx @@ -17,7 +17,7 @@ * under the License. */ import { List } from 'immutable'; -import PropTypes from 'prop-types'; +// @ts-ignore import JSONbig from 'json-bigint'; import React, { PureComponent } from 'react'; import JSONTree from 'react-json-tree'; @@ -28,6 +28,7 @@ import { SortDirection, SortIndicator, Table, + SortDirectionType, } from 'react-virtualized'; import { getMultipleTextDimensions } from '@superset-ui/dimension'; import { t } from '@superset-ui/translation'; @@ -37,7 +38,9 @@ import CopyToClipboard from '../CopyToClipboard'; import ModalTrigger from '../ModalTrigger'; import TooltipWrapper from '../TooltipWrapper'; -function safeJsonObjectParse(data) { +function safeJsonObjectParse( + data: unknown, +): null | unknown[] | Record { // First perform a cheap proxy to avoid calling JSON.parse on data that is clearly not a // JSON object or array if ( @@ -85,31 +88,43 @@ const JSON_TREE_THEME = { // when more than MAX_COLUMNS_FOR_TABLE are returned, switch from table to grid view export const MAX_COLUMNS_FOR_TABLE = 50; -const propTypes = { - orderedColumnKeys: PropTypes.array.isRequired, - 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, -}; +type CellDataType = string | number | null; +type Datum = Record; -const defaultProps = { - filterText: '', - headerHeight: 32, - overscanColumnCount: 10, - overscanRowCount: 10, - rowHeight: 32, - striped: true, - expandedColumns: [], -}; +interface FilterableTableProps { + orderedColumnKeys: string[]; + data: Record[]; + height: number; + filterText: string; + headerHeight: number; + overscanColumnCount: number; + overscanRowCount: number; + rowHeight: number; + striped: boolean; + expandedColumns: string[]; +} -export default class FilterableTable extends PureComponent { - constructor(props) { +interface FilterableTableState { + 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); this.list = List(this.formatTableData(props.data)); this.addJsonModal = this.addJsonModal.bind(this); @@ -140,7 +155,6 @@ export default class FilterableTable extends PureComponent { this.totalTableHeight = props.height; this.state = { - sortBy: null, sortDirection: SortDirection.ASC, fitted: false, }; @@ -152,7 +166,7 @@ export default class FilterableTable extends PureComponent { this.fitTableToWidthIfNeeded(); } - getDatum(list, index) { + getDatum(list: List, index: number) { return list.get(index % list.size); } @@ -162,9 +176,10 @@ export default class FilterableTable extends PureComponent { const cellContent = [].concat( ...this.props.orderedColumnKeys.map(key => this.list - .map(data => + .map((data: Datum) => this.getCellContent({ cellData: data[key], columnKey: key }), ) + // @ts-ignore .push(key) .toJS(), ), @@ -191,7 +206,13 @@ export default class FilterableTable extends PureComponent { return widthsByColumnKey; } - getCellContent({ cellData, columnKey }) { + getCellContent({ + cellData, + columnKey, + }: { + cellData: CellDataType; + columnKey: string; + }) { if (cellData === null) { return NULL; } @@ -208,7 +229,14 @@ export default class FilterableTable extends PureComponent { return this.complexColumns[columnKey] ? truncated : content; } - formatTableData(data) { + list: List; + complexColumns: Record; + widthsForColumnsByKey: Record; + totalTableWidth: number; + totalTableHeight: number; + container: React.RefObject; + + formatTableData(data: Record[]): Datum[] { const formattedData = data.map(row => { const newRow = {}; for (const k in row) { @@ -224,7 +252,7 @@ export default class FilterableTable extends PureComponent { return formattedData; } - hasMatch(text, row) { + hasMatch(text: string, row: Datum) { const values = []; for (const key in row) { if (row.hasOwnProperty(key)) { @@ -243,7 +271,7 @@ export default class FilterableTable extends PureComponent { return values.some(v => v.includes(lowerCaseText)); } - rowClassName({ index }) { + rowClassName({ index }: { index: number }) { let className = ''; if (this.props.striped) { className = index % 2 === 0 ? 'even-row' : 'odd-row'; @@ -251,12 +279,18 @@ export default class FilterableTable extends PureComponent { return className; } - sort({ sortBy, sortDirection }) { + sort({ + sortBy, + sortDirection, + }: { + sortBy: string; + sortDirection: SortDirectionType; + }) { this.setState({ sortBy, sortDirection }); } fitTableToWidthIfNeeded() { - const containerWidth = this.container.clientWidth; + const containerWidth = this.container.current!.clientWidth; if (this.totalTableWidth < containerWidth) { // fit table width if content doesn't fill the width of the container this.totalTableWidth = containerWidth; @@ -264,7 +298,11 @@ export default class FilterableTable extends PureComponent { this.setState({ fitted: true }); } - addJsonModal(node, jsonObject, jsonString) { + addJsonModal( + node: React.ReactNode, + jsonObject: Record | unknown[], + jsonString: CellDataType, + ) { return ( } @@ -279,24 +317,36 @@ export default class FilterableTable extends PureComponent { ); } - sortResults(sortBy, descending) { - return (a, b) => { - if (a[sortBy] === b[sortBy]) { + sortResults(sortBy: string, descending: boolean) { + return (a: Datum, b: Datum) => { + const aValue = a[sortBy]; + const bValue = b[sortBy]; + if (aValue === bValue) { // equal items sort equally return 0; - } else if (a[sortBy] === null) { + } else if (aValue === null) { // nulls sort after anything else return 1; - } else if (b[sortBy] === null) { + } else if (bValue === null) { return -1; } 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 = this.props.expandedColumns.indexOf(label) > -1 ? '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 className = this.props.expandedColumns.indexOf(label) > -1 @@ -322,7 +380,13 @@ export default class FilterableTable extends PureComponent { return (
{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 cellData = this.list.get(rowIndex)[columnKey]; const content = this.getCellContent({ cellData, columnKey }); const cellNode = (
{content} @@ -362,14 +442,17 @@ export default class FilterableTable extends PureComponent { let { height } = this.props; 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 // and the height of the table container if the content overflows height -= SCROLL_BAR_HEIGHT; totalTableHeight -= SCROLL_BAR_HEIGHT; } - const getColumnWidth = ({ index }) => + const getColumnWidth = ({ index }: { index: number }) => this.widthsForColumnsByKey[orderedColumnKeys[index]]; // 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 jsonObject = safeJsonObjectParse(cellData); if (jsonObject) { @@ -432,30 +521,33 @@ export default class FilterableTable extends PureComponent { rowHeight, } = this.props; - let sortedAndFilteredList = this.list; + let sortedAndFilteredList: List = this.list; // filter list if (filterText) { - sortedAndFilteredList = this.list.filter(row => + sortedAndFilteredList = this.list.filter((row: Datum) => this.hasMatch(filterText, row), - ); + ) as List; } // sort list if (sortBy) { sortedAndFilteredList = sortedAndFilteredList.sort( this.sortResults(sortBy, sortDirection === SortDirection.DESC), - ); + ) as List; } let { height } = this.props; 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 // and the height of the table container if the content overflows height -= SCROLL_BAR_HEIGHT; totalTableHeight -= SCROLL_BAR_HEIGHT; } - const rowGetter = ({ index }) => + const rowGetter = ({ index }: { index: number }) => this.getDatum(sortedAndFilteredList, index); return (