You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by er...@apache.org on 2020/06/19 04:57:29 UTC
[incubator-superset] branch master updated: chore: type
FilterableTable (#10073)
This is an automated email from the ASF dual-hosted git repository.
erikrit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 2e76fbb chore: type FilterableTable (#10073)
2e76fbb is described below
commit 2e76fbb7e56f4551d3733db82ce5a2e04ad6cd8d
Author: Erik Ritter <er...@airbnb.com>
AuthorDate: Thu Jun 18 21:57:11 2020 -0700
chore: type FilterableTable (#10073)
---
superset-frontend/package-lock.json | 26 +++
superset-frontend/package.json | 2 +
...ableTable_spec.jsx => FilterableTable_spec.tsx} | 8 +-
.../{FilterableTable.jsx => FilterableTable.tsx} | 207 +++++++++++++++------
4 files changed, 180 insertions(+), 63 deletions(-)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index f80cb17..cc3557c 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 7d60f6b..921cb81 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 74cbf92..2d2b451 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(<FilterableTable {...mockedProps} />);
});
@@ -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 5b0e111..236f25d 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<string, unknown> {
// 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<string, CellDataType>;
+
+interface FilterableTableProps {
+ orderedColumnKeys: string[];
+ data: Record<string, unknown>[];
+ height: number;
+ filterText: string;
+ headerHeight: number;
+ overscanColumnCount: number;
+ overscanRowCount: number;
+ rowHeight: number;
+ striped: boolean;
+ expandedColumns: string[];
+}
-const defaultProps = {
- filterText: '',
- headerHeight: 32,
- overscanColumnCount: 10,
- overscanRowCount: 10,
- rowHeight: 32,
- striped: true,
- expandedColumns: [],
-};
+interface FilterableTableState {
+ sortBy?: string;
+ sortDirection: SortDirectionType;
+ fitted: boolean;
+}
-export default class FilterableTable extends PureComponent {
- constructor(props) {
+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<Datum>, 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 <i className="text-muted">NULL</i>;
}
@@ -208,7 +229,14 @@ export default class FilterableTable extends PureComponent {
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 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<string, unknown> | unknown[],
+ jsonString: CellDataType,
+ ) {
return (
<ModalTrigger
modalBody={<JSONTree data={jsonObject} theme={JSON_TREE_THEME} />}
@@ -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 (
<TooltipWrapper key={key} label="header" tooltip={label}>
<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`}
>
{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 = (
<div
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 })}`}
>
{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<Datum> = this.list;
// filter list
if (filterText) {
- sortedAndFilteredList = this.list.filter(row =>
+ sortedAndFilteredList = this.list.filter((row: Datum) =>
this.hasMatch(filterText, row),
- );
+ ) as List<Datum>;
}
// sort list
if (sortBy) {
sortedAndFilteredList = sortedAndFilteredList.sort(
this.sortResults(sortBy, sortDirection === SortDirection.DESC),
- );
+ ) as List<Datum>;
}
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 (
<div
@@ -504,6 +596,3 @@ export default class FilterableTable extends PureComponent {
return this.renderTable();
}
}
-
-FilterableTable.propTypes = propTypes;
-FilterableTable.defaultProps = defaultProps;