feat(explore): Allow using time formatter on temporal columns in data table (#18569)

* feat(explore): Allow using time formatter on temporal columns in data table

* Fix data table loading

* Return colnames and coltypes from results request

* Fix types

* Fix tests

* Fix copy button

* Fix df is none

* Fix test

* Address comments

* Move useTimeFormattedColumns out of useTableColumns

* Make reducer more readable
This commit is contained in:
Kamil Gabryjelski
2022-02-09 10:29:11 +01:00
committed by GitHub
parent 28e729b835
commit 830f2e71d3
14 changed files with 475 additions and 88 deletions

View File

@@ -105,7 +105,13 @@ test('Should copy data table content correctly', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
{
result: [{ data: [{ __timestamp: 1230768000000, genre: 'Action' }] }],
result: [
{
data: [{ __timestamp: 1230768000000, genre: 'Action' }],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
},
);
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
@@ -118,12 +124,20 @@ test('Should copy data table content correctly', async () => {
queriesResponse: [
{
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
}}
/>,
{
useRedux: true,
initialState: {
explore: {
timeFormattedColumns: {
'34__table': ['__timestamp'],
},
},
},
},
);
userEvent.click(await screen.findByText('Data'));

View File

@@ -17,7 +17,13 @@
* under the License.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { JsonObject, styled, t } from '@superset-ui/core';
import {
ensureIsArray,
GenericDataType,
JsonObject,
styled,
t,
} from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import Tabs from 'src/components/Tabs';
import Loading from 'src/components/Loading';
@@ -37,16 +43,17 @@ import {
useTableColumns,
} from 'src/explore/components/DataTableControl';
import { applyFormattingToTabularData } from 'src/utils/common';
import { useTimeFormattedColumns } from '../useTimeFormattedColumns';
const RESULT_TYPES = {
results: 'results' as const,
samples: 'samples' as const,
};
const NULLISH_RESULTS_STATE = {
[RESULT_TYPES.results]: undefined,
[RESULT_TYPES.samples]: undefined,
};
const getDefaultDataTablesState = (value: any) => ({
[RESULT_TYPES.results]: value,
[RESULT_TYPES.samples]: value,
});
const DATA_TABLE_PAGE_SIZE = 50;
@@ -105,8 +112,11 @@ const Error = styled.pre`
interface DataTableProps {
columnNames: string[];
columnTypes: GenericDataType[] | undefined;
datasource: string | undefined;
filterText: string;
data: object[] | undefined;
timeFormattedColumns: string[] | undefined;
isLoading: boolean;
error: string | undefined;
errorMessage: React.ReactElement | undefined;
@@ -114,15 +124,24 @@ interface DataTableProps {
const DataTable = ({
columnNames,
columnTypes,
datasource,
filterText,
data,
timeFormattedColumns,
isLoading,
error,
errorMessage,
}: DataTableProps) => {
// this is to preserve the order of the columns, even if there are integer values,
// while also only grabbing the first column's keys
const columns = useTableColumns(columnNames, data);
const columns = useTableColumns(
columnNames,
columnTypes,
data,
datasource,
timeFormattedColumns,
);
const filteredData = useFilteredTableData(filterText, data);
if (isLoading) {
@@ -172,48 +191,42 @@ export const DataTablesPane = ({
errorMessage?: JSX.Element;
queriesResponse: Record<string, any>;
}) => {
const [data, setData] = useState<{
[RESULT_TYPES.results]?: Record<string, any>[];
[RESULT_TYPES.samples]?: Record<string, any>[];
}>(NULLISH_RESULTS_STATE);
const [isLoading, setIsLoading] = useState({
[RESULT_TYPES.results]: true,
[RESULT_TYPES.samples]: true,
});
const [columnNames, setColumnNames] = useState<{
[RESULT_TYPES.results]: string[];
[RESULT_TYPES.samples]: string[];
}>({
[RESULT_TYPES.results]: [],
[RESULT_TYPES.samples]: [],
});
const [error, setError] = useState(NULLISH_RESULTS_STATE);
const [data, setData] = useState(getDefaultDataTablesState(undefined));
const [isLoading, setIsLoading] = useState(getDefaultDataTablesState(true));
const [columnNames, setColumnNames] = useState(getDefaultDataTablesState([]));
const [columnTypes, setColumnTypes] = useState(getDefaultDataTablesState([]));
const [error, setError] = useState(getDefaultDataTablesState(''));
const [filterText, setFilterText] = useState('');
const [activeTabKey, setActiveTabKey] = useState<string>(
RESULT_TYPES.results,
);
const [isRequestPending, setIsRequestPending] = useState<{
[RESULT_TYPES.results]?: boolean;
[RESULT_TYPES.samples]?: boolean;
}>(NULLISH_RESULTS_STATE);
const [isRequestPending, setIsRequestPending] = useState(
getDefaultDataTablesState(false),
);
const [panelOpen, setPanelOpen] = useState(
getItem(LocalStorageKeys.is_datapanel_open, false),
);
const timeFormattedColumns = useTimeFormattedColumns(
queryFormData?.datasource,
);
const formattedData = useMemo(
() => ({
[RESULT_TYPES.results]: applyFormattingToTabularData(
data[RESULT_TYPES.results],
timeFormattedColumns,
),
[RESULT_TYPES.samples]: applyFormattingToTabularData(
data[RESULT_TYPES.samples],
timeFormattedColumns,
),
}),
[data],
[data, timeFormattedColumns],
);
const getData = useCallback(
(resultType: string) => {
(resultType: 'samples' | 'results') => {
setIsLoading(prevIsLoading => ({
...prevIsLoading,
[resultType]: true,
@@ -247,12 +260,16 @@ export const DataTablesPane = ({
[resultType]: json.result[0].data,
}));
}
const checkCols = json?.result[0]?.data?.length
? Object.keys(json.result[0].data[0])
: null;
const colNames = ensureIsArray(json.result[0].colnames);
setColumnNames(prevColumnNames => ({
...prevColumnNames,
[resultType]: json.result[0].columns || checkCols,
[resultType]: colNames,
}));
setColumnTypes(prevColumnTypes => ({
...prevColumnTypes,
[resultType]: json.result[0].coltypes || [],
}));
setIsLoading(prevIsLoading => ({
...prevIsLoading,
@@ -260,14 +277,14 @@ export const DataTablesPane = ({
}));
setError(prevError => ({
...prevError,
[resultType]: null,
[resultType]: undefined,
}));
})
.catch(response => {
getClientErrorObject(response).then(({ error, message }) => {
setError(prevError => ({
...prevError,
[resultType]: error || message || t('Sorry, An error occurred'),
[resultType]: error || message || t('Sorry, an error occurred'),
}));
setIsLoading(prevIsLoading => ({
...prevIsLoading,
@@ -295,14 +312,14 @@ export const DataTablesPane = ({
...prevState,
[RESULT_TYPES.samples]: true,
}));
}, [queryFormData?.adhoc_filters, queryFormData?.datasource]);
}, [queryFormData?.datasource]);
useEffect(() => {
if (queriesResponse && chartStatus === 'success') {
const { colnames } = queriesResponse[0];
setColumnNames(prevColumnNames => ({
...prevColumnNames,
[RESULT_TYPES.results]: colnames ? [...colnames] : [],
[RESULT_TYPES.results]: colnames ?? [],
}));
}
}, [queriesResponse, chartStatus]);
@@ -396,7 +413,10 @@ export const DataTablesPane = ({
<DataTable
isLoading={isLoading[RESULT_TYPES.results]}
data={data[RESULT_TYPES.results]}
datasource={queryFormData?.datasource}
timeFormattedColumns={timeFormattedColumns}
columnNames={columnNames[RESULT_TYPES.results]}
columnTypes={columnTypes[RESULT_TYPES.results]}
filterText={filterText}
error={error[RESULT_TYPES.results]}
errorMessage={errorMessage}
@@ -409,7 +429,10 @@ export const DataTablesPane = ({
<DataTable
isLoading={isLoading[RESULT_TYPES.samples]}
data={data[RESULT_TYPES.samples]}
datasource={queryFormData?.datasource}
timeFormattedColumns={timeFormattedColumns}
columnNames={columnNames[RESULT_TYPES.samples]}
columnTypes={columnTypes[RESULT_TYPES.samples]}
filterText={filterText}
error={error[RESULT_TYPES.samples]}
errorMessage={errorMessage}