| unknown[],
jsonString: CellDataType,
@@ -444,273 +316,81 @@ const FilterableTable = ({
return value;
};
- 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) {
- return 0;
- }
-
- // nulls sort after anything else
- if (aValue === null) {
- return 1;
- }
- if (bValue === null) {
- return -1;
- }
+ const sortResults = (key: string, a: Datum, b: Datum) => {
+ const aValue = parseNumberFromString(a[key]);
+ const bValue = parseNumberFromString(b[key]);
- if (descending) {
- return aValue < bValue ? 1 : -1;
- }
- return aValue < bValue ? -1 : 1;
- };
-
- const sortGrid = (label: string) => {
- sort({
- sortBy: label,
- sortDirection:
- sortDirectionState === SortDirection.DESC || sortByState !== label
- ? SortDirection.ASC
- : SortDirection.DESC,
- });
- };
+ // equal items sort equally
+ if (aValue === bValue) {
+ return 0;
+ }
- const renderTableHeader = ({
- dataKey,
- label,
- sortBy,
- sortDirection,
- }: {
- dataKey: string;
- label: string;
- sortBy: string;
- sortDirection: SortDirectionType;
- }) => {
- const className =
- expandedColumns.indexOf(label) > -1
- ? 'header-style-disabled'
- : 'header-style';
-
- return (
-
- {label}
- {sortBy === dataKey && }
-
- );
- };
+ // nulls sort after anything else
+ if (aValue === null) {
+ return 1;
+ }
+ if (bValue === null) {
+ return -1;
+ }
- const renderGridCellHeader = ({
- columnIndex,
- key,
- style,
- }: {
- columnIndex: number;
- key: string;
- style: React.CSSProperties;
- }) => {
- const label = orderedColumnKeys[columnIndex];
- const className =
- expandedColumns.indexOf(label) > -1
- ? 'header-style-disabled'
- : 'header-style';
- return (
- sortGrid(label)}
- >
- {label}
- {sortByState === label && (
-
- )}
-
- );
+ return aValue < bValue ? -1 : 1;
};
- const renderGridCell = ({
- columnIndex,
- key,
- rowIndex,
- style,
- }: {
- columnIndex: number;
- key: string;
- rowIndex: number;
- style: React.CSSProperties;
- }) => {
- const columnKey = orderedColumnKeys[columnIndex];
- const cellData = displayedList[rowIndex][columnKey];
- const cellText = getCellContent({ cellData, columnKey });
- const content =
- cellData === null ? {cellText} : cellText;
- const cellNode = (
-
- );
-
- const jsonObject = safeJsonObjectParse(cellData);
- if (jsonObject) {
- return addJsonModal(cellNode, jsonObject, cellData);
- }
- return cellNode;
- };
+ const keyword = useDebounceValue(filterText);
- const renderGrid = () => {
- // 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
- const totalTableHeight =
- container.current &&
- totalTableWidth.current > container.current.clientWidth
- ? height - SCROLL_BAR_HEIGHT
- : height;
-
- const getColumnWidth = ({ index }: { index: number }) =>
- widthsForColumnsByKey[orderedColumnKeys[index]];
-
- // fix height of filterable table
- return (
-
-
- {({ onScroll, scrollLeft }) => (
- <>
-
- {({ width }) => (
-
-
-
-
- )}
-
- >
- )}
-
-
- );
- };
+ const filteredList = useMemo(
+ () =>
+ keyword ? list.filter((row: Datum) => hasMatch(keyword, row)) : list,
+ [list, keyword],
+ );
- const renderTableCell = ({
- cellData,
- columnKey,
- }: {
- cellData: CellDataType;
- columnKey: string;
- }) => {
+ const renderTableCell = (cellData: CellDataType, columnKey: string) => {
const cellNode = getCellContent({ cellData, columnKey });
const content =
cellData === null ? {cellNode} : cellNode;
const jsonObject = safeJsonObjectParse(cellData);
if (jsonObject) {
- return addJsonModal(cellNode, jsonObject, cellData);
+ return renderJsonModal(cellNode, jsonObject, cellData);
}
return content;
};
- const renderTable = () => {
- let sortedAndFilteredList = displayedList;
- // filter list
- if (filterText) {
- sortedAndFilteredList = sortedAndFilteredList.filter((row: Datum) =>
- hasMatch(filterText, row),
- );
- }
-
- // 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
- const totalTableHeight =
- container.current &&
- totalTableWidth.current > container.current.clientWidth
- ? height - SCROLL_BAR_HEIGHT
- : height;
-
- const rowGetter = ({ index }: { index: number }) =>
- getDatum(sortedAndFilteredList, index);
- return (
-
- {fitted && (
-
- {orderedColumnKeys.map(columnKey => (
-
- renderTableCell({ cellData, columnKey })
- }
- dataKey={columnKey}
- disableSort={false}
- headerRenderer={renderTableHeader}
- width={widthsForColumnsByKey[columnKey]}
- label={columnKey}
- key={columnKey}
- />
- ))}
-
- )}
-
- );
- };
+ // 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
+ const totalTableHeight =
+ container.current && totalTableWidth.current > container.current.clientWidth
+ ? height - SCROLL_BAR_HEIGHT
+ : height;
- if (orderedColumnKeys.length > MAX_COLUMNS_FOR_TABLE) {
- return renderGrid();
- }
- return renderTable();
+ const columns = orderedColumnKeys.map(key => ({
+ key,
+ title: key,
+ dataIndex: key,
+ width: widthsForColumnsByKey[key],
+ sorter: (a: Datum, b: Datum) => sortResults(key, a, b),
+ render: (text: CellDataType) => renderTableCell(text, key),
+ }));
+
+ return (
+
+ {fitted && (
+
+ )}
+
+ );
};
export default FilterableTable;
diff --git a/superset-frontend/src/components/Table/index.tsx b/superset-frontend/src/components/Table/index.tsx
index 662908a7508b..e5ce24c64eb2 100644
--- a/superset-frontend/src/components/Table/index.tsx
+++ b/superset-frontend/src/components/Table/index.tsx
@@ -58,6 +58,10 @@ export interface TableProps {
* Data that will populate the each row and map to the column key.
*/
data: RecordType[];
+ /**
+ * Whether to show all table borders
+ */
+ bordered?: boolean;
/**
* Table column definitions.
*/
@@ -224,6 +228,7 @@ export function Table(
) {
const {
data,
+ bordered,
columns,
selectedRows = defaultRowSelection,
handleRowSelection,
@@ -376,6 +381,7 @@ export function Table(
onRow,
theme,
height: bodyHeight,
+ bordered,
};
return (
@@ -391,7 +397,14 @@ export function Table(
{virtualize && (
)}
diff --git a/superset-frontend/src/hooks/useDebounceValue.test.ts b/superset-frontend/src/hooks/useDebounceValue.test.ts
new file mode 100644
index 000000000000..b0bdff34ddc1
--- /dev/null
+++ b/superset-frontend/src/hooks/useDebounceValue.test.ts
@@ -0,0 +1,81 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { act, renderHook } from '@testing-library/react-hooks';
+import { useDebounceValue } from './useDebounceValue';
+
+afterEach(() => {
+ jest.clearAllTimers();
+ jest.useRealTimers();
+});
+
+test('should return the initial value', () => {
+ const { result } = renderHook(() => useDebounceValue('hello'));
+ expect(result.current).toBe('hello');
+});
+
+test('should update debounced value after delay', async () => {
+ jest.useFakeTimers();
+ const { result, rerender } = renderHook(
+ ({ value, delay }) => useDebounceValue(value, delay),
+ { initialProps: { value: 'hello', delay: 1000 } },
+ );
+
+ expect(result.current).toBe('hello');
+ act(() => {
+ rerender({ value: 'world', delay: 1000 });
+ jest.advanceTimersByTime(500);
+ });
+
+ expect(result.current).toBe('hello');
+
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+
+ expect(result.current).toBe('world');
+});
+
+it('should cancel previous timeout when value changes', async () => {
+ jest.useFakeTimers();
+ const { result, rerender } = renderHook(
+ ({ value, delay }) => useDebounceValue(value, delay),
+ { initialProps: { value: 'hello', delay: 1000 } },
+ );
+
+ expect(result.current).toBe('hello');
+ rerender({ value: 'world', delay: 1000 });
+
+ jest.advanceTimersByTime(500);
+ rerender({ value: 'foo', delay: 1000 });
+
+ jest.advanceTimersByTime(500);
+ expect(result.current).toBe('hello');
+});
+
+test('should cancel the timeout when unmounting', async () => {
+ jest.useFakeTimers();
+ const { result, unmount } = renderHook(() => useDebounceValue('hello', 1000));
+
+ expect(result.current).toBe('hello');
+ unmount();
+
+ jest.advanceTimersByTime(1000);
+ expect(clearTimeout).toHaveBeenCalled();
+});
diff --git a/superset-frontend/src/hooks/useDebounceValue.ts b/superset-frontend/src/hooks/useDebounceValue.ts
new file mode 100644
index 000000000000..711b2dbd5a98
--- /dev/null
+++ b/superset-frontend/src/hooks/useDebounceValue.ts
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, useEffect } from 'react';
+import { FAST_DEBOUNCE } from 'src/constants';
+
+export function useDebounceValue(value: string, delay = FAST_DEBOUNCE) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const handler: NodeJS.Timeout = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+
+ // Cancel the timeout if value changes (also on delay change or unmount)
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
diff --git a/superset-frontend/src/types/react-table-config.d.ts b/superset-frontend/src/types/react-table-config.d.ts
index aa932c85997b..7f769c36d6bf 100644
--- a/superset-frontend/src/types/react-table-config.d.ts
+++ b/superset-frontend/src/types/react-table-config.d.ts
@@ -64,7 +64,6 @@ import {
UseSortByOptions,
UseSortByState,
} from 'react-table';
-import { ColumnSizer } from 'react-virtualized';
declare module 'react-table' {
type ColumnSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js
index f8428e4418fb..cf61a352ed63 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -250,7 +250,6 @@ const config = {
'react-hot-loader',
'react-select',
'react-sortable-hoc',
- 'react-virtualized',
'react-table',
'react-ace',
'@hot-loader.*',