mirror of
https://github.com/apache/superset.git
synced 2026-05-30 04:39:20 +00:00
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:
committed by
GitHub
parent
28e729b835
commit
830f2e71d3
@@ -16,20 +16,37 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
css,
|
||||
GenericDataType,
|
||||
getTimeFormatter,
|
||||
styled,
|
||||
t,
|
||||
TimeFormats,
|
||||
useTheme,
|
||||
} from '@superset-ui/core';
|
||||
import { Global } from '@emotion/react';
|
||||
import { Column } from 'react-table';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { Input } from 'src/common/components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Input, Space } from 'src/common/components';
|
||||
import {
|
||||
BOOL_FALSE_DISPLAY,
|
||||
BOOL_TRUE_DISPLAY,
|
||||
SLOW_DEBOUNCE,
|
||||
} from 'src/constants';
|
||||
import { Radio } from 'src/components/Radio';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Button from 'src/components/Button';
|
||||
import Popover from 'src/components/Popover';
|
||||
import { prepareCopyToClipboardTabularData } from 'src/utils/common';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import RowCountLabel from 'src/explore/components/RowCountLabel';
|
||||
import {
|
||||
setTimeFormattedColumn,
|
||||
unsetTimeFormattedColumn,
|
||||
} from 'src/explore/actions/exploreActions';
|
||||
|
||||
export const CopyButton = styled(Button)`
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
@@ -97,6 +114,129 @@ export const RowCount = ({
|
||||
/>
|
||||
);
|
||||
|
||||
enum FormatPickerValue {
|
||||
Formatted,
|
||||
Original,
|
||||
}
|
||||
|
||||
const FormatPicker = ({
|
||||
onChange,
|
||||
value,
|
||||
}: {
|
||||
onChange: any;
|
||||
value: FormatPickerValue;
|
||||
}) => (
|
||||
<Radio.Group value={value} onChange={onChange}>
|
||||
<Space direction="vertical">
|
||||
<Radio value={FormatPickerValue.Original}>{t('Original value')}</Radio>
|
||||
<Radio value={FormatPickerValue.Formatted}>{t('Formatted date')}</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
);
|
||||
|
||||
const FormatPickerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: ${({ theme }) => `${theme.gridUnit * 4}px`};
|
||||
`;
|
||||
|
||||
const FormatPickerLabel = styled.span`
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const DataTableTemporalHeaderCell = ({
|
||||
columnName,
|
||||
datasourceId,
|
||||
timeFormattedColumnIndex,
|
||||
}: {
|
||||
columnName: string;
|
||||
datasourceId?: string;
|
||||
timeFormattedColumnIndex: number;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const isColumnTimeFormatted = timeFormattedColumnIndex > -1;
|
||||
|
||||
const onChange = useCallback(
|
||||
e => {
|
||||
if (!datasourceId) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.target.value === FormatPickerValue.Original &&
|
||||
isColumnTimeFormatted
|
||||
) {
|
||||
dispatch(
|
||||
unsetTimeFormattedColumn(datasourceId, timeFormattedColumnIndex),
|
||||
);
|
||||
} else if (
|
||||
e.target.value === FormatPickerValue.Formatted &&
|
||||
!isColumnTimeFormatted
|
||||
) {
|
||||
dispatch(setTimeFormattedColumn(datasourceId, columnName));
|
||||
}
|
||||
},
|
||||
[
|
||||
timeFormattedColumnIndex,
|
||||
columnName,
|
||||
datasourceId,
|
||||
dispatch,
|
||||
isColumnTimeFormatted,
|
||||
],
|
||||
);
|
||||
const overlayContent = useMemo(
|
||||
() =>
|
||||
datasourceId ? ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<FormatPickerContainer onClick={e => e.stopPropagation()}>
|
||||
{/* hack to disable click propagation from popover content to table header, which triggers sorting column */}
|
||||
<Global
|
||||
styles={css`
|
||||
.column-formatting-popover .ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<FormatPickerLabel>{t('Column Formatting')}</FormatPickerLabel>
|
||||
<FormatPicker
|
||||
onChange={onChange}
|
||||
value={
|
||||
isColumnTimeFormatted
|
||||
? FormatPickerValue.Formatted
|
||||
: FormatPickerValue.Original
|
||||
}
|
||||
/>
|
||||
</FormatPickerContainer>
|
||||
) : null,
|
||||
[datasourceId, isColumnTimeFormatted, onChange],
|
||||
);
|
||||
|
||||
return datasourceId ? (
|
||||
<span>
|
||||
<Popover
|
||||
overlayClassName="column-formatting-popover"
|
||||
trigger="click"
|
||||
content={overlayContent}
|
||||
placement="bottomLeft"
|
||||
arrowPointAtCenter
|
||||
>
|
||||
<Icons.SettingOutlined
|
||||
iconSize="m"
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
css={{ marginRight: `${theme.gridUnit}px` }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
</Popover>
|
||||
{columnName}
|
||||
</span>
|
||||
) : (
|
||||
<span>{columnName}</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFilteredTableData = (
|
||||
filterText: string,
|
||||
data?: Record<string, any>[],
|
||||
@@ -121,9 +261,14 @@ export const useFilteredTableData = (
|
||||
}, [data, filterText, rowsAsStrings]);
|
||||
};
|
||||
|
||||
const timeFormatter = getTimeFormatter(TimeFormats.DATABASE_DATETIME);
|
||||
|
||||
export const useTableColumns = (
|
||||
colnames?: string[],
|
||||
coltypes?: GenericDataType[],
|
||||
data?: Record<string, any>[],
|
||||
datasourceId?: string,
|
||||
timeFormattedColumns: string[] = [],
|
||||
moreConfigs?: { [key: string]: Partial<Column> },
|
||||
) =>
|
||||
useMemo(
|
||||
@@ -131,24 +276,40 @@ export const useTableColumns = (
|
||||
colnames && data?.length
|
||||
? colnames
|
||||
.filter((column: string) => Object.keys(data[0]).includes(column))
|
||||
.map(
|
||||
key =>
|
||||
({
|
||||
accessor: row => row[key],
|
||||
// When the key is empty, have to give a string of length greater than 0
|
||||
Header: key || ' ',
|
||||
Cell: ({ value }) => {
|
||||
if (value === true) {
|
||||
return BOOL_TRUE_DISPLAY;
|
||||
}
|
||||
if (value === false) {
|
||||
return BOOL_FALSE_DISPLAY;
|
||||
}
|
||||
return String(value);
|
||||
},
|
||||
...moreConfigs?.[key],
|
||||
} as Column),
|
||||
)
|
||||
.map((key, index) => {
|
||||
const timeFormattedColumnIndex =
|
||||
coltypes?.[index] === GenericDataType.TEMPORAL
|
||||
? timeFormattedColumns.indexOf(key)
|
||||
: -1;
|
||||
return {
|
||||
id: key,
|
||||
accessor: row => row[key],
|
||||
// When the key is empty, have to give a string of length greater than 0
|
||||
Header:
|
||||
coltypes?.[index] === GenericDataType.TEMPORAL ? (
|
||||
<DataTableTemporalHeaderCell
|
||||
columnName={key}
|
||||
datasourceId={datasourceId}
|
||||
timeFormattedColumnIndex={timeFormattedColumnIndex}
|
||||
/>
|
||||
) : (
|
||||
key
|
||||
),
|
||||
Cell: ({ value }) => {
|
||||
if (value === true) {
|
||||
return BOOL_TRUE_DISPLAY;
|
||||
}
|
||||
if (value === false) {
|
||||
return BOOL_FALSE_DISPLAY;
|
||||
}
|
||||
if (timeFormattedColumnIndex > -1) {
|
||||
return timeFormatter(value);
|
||||
}
|
||||
return String(value);
|
||||
},
|
||||
...moreConfigs?.[key],
|
||||
} as Column;
|
||||
})
|
||||
: [],
|
||||
[data, colnames, moreConfigs],
|
||||
[colnames, data, coltypes, datasourceId, moreConfigs, timeFormattedColumns],
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { GenericDataType } from '@superset-ui/core';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { BOOL_FALSE_DISPLAY, BOOL_TRUE_DISPLAY } from 'src/constants';
|
||||
import { useTableColumns } from '.';
|
||||
@@ -43,29 +44,39 @@ const data = [
|
||||
},
|
||||
];
|
||||
const all_columns = ['col01', 'col02', 'col03', asciiKey, unicodeKey];
|
||||
const coltypes = [
|
||||
GenericDataType.BOOLEAN,
|
||||
GenericDataType.BOOLEAN,
|
||||
GenericDataType.STRING,
|
||||
GenericDataType.STRING,
|
||||
];
|
||||
|
||||
test('useTableColumns with no options', () => {
|
||||
const hook = renderHook(() => useTableColumns(all_columns, data));
|
||||
const hook = renderHook(() => useTableColumns(all_columns, coltypes, data));
|
||||
expect(hook.result.current).toEqual([
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: 'col01',
|
||||
accessor: expect.any(Function),
|
||||
id: 'col01',
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: 'col02',
|
||||
accessor: expect.any(Function),
|
||||
id: 'col02',
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: asciiKey,
|
||||
accessor: expect.any(Function),
|
||||
id: asciiKey,
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: unicodeKey,
|
||||
accessor: expect.any(Function),
|
||||
id: unicodeKey,
|
||||
},
|
||||
]);
|
||||
hook.result.current.forEach((col: JsonObject) => {
|
||||
@@ -84,32 +95,39 @@ test('useTableColumns with no options', () => {
|
||||
|
||||
test('use only the first record columns', () => {
|
||||
const newData = [data[3], data[0]];
|
||||
const hook = renderHook(() => useTableColumns(all_columns, newData));
|
||||
const hook = renderHook(() =>
|
||||
useTableColumns(all_columns, coltypes, newData),
|
||||
);
|
||||
expect(hook.result.current).toEqual([
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: 'col01',
|
||||
accessor: expect.any(Function),
|
||||
id: 'col01',
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: 'col02',
|
||||
accessor: expect.any(Function),
|
||||
id: 'col02',
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: 'col03',
|
||||
accessor: expect.any(Function),
|
||||
id: 'col03',
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: asciiKey,
|
||||
accessor: expect.any(Function),
|
||||
id: asciiKey,
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: unicodeKey,
|
||||
accessor: expect.any(Function),
|
||||
id: unicodeKey,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -136,7 +154,9 @@ test('use only the first record columns', () => {
|
||||
|
||||
test('useTableColumns with options', () => {
|
||||
const hook = renderHook(() =>
|
||||
useTableColumns(all_columns, data, { col01: { id: 'ID' } }),
|
||||
useTableColumns(all_columns, coltypes, data, undefined, [], {
|
||||
col01: { id: 'ID' },
|
||||
}),
|
||||
);
|
||||
expect(hook.result.current).toEqual([
|
||||
{
|
||||
@@ -149,16 +169,19 @@ test('useTableColumns with options', () => {
|
||||
Cell: expect.any(Function),
|
||||
Header: 'col02',
|
||||
accessor: expect.any(Function),
|
||||
id: 'col02',
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: asciiKey,
|
||||
accessor: expect.any(Function),
|
||||
id: asciiKey,
|
||||
},
|
||||
{
|
||||
Cell: expect.any(Function),
|
||||
Header: unicodeKey,
|
||||
accessor: expect.any(Function),
|
||||
id: unicodeKey,
|
||||
},
|
||||
]);
|
||||
hook.result.current.forEach((col: JsonObject) => {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from '../reducers/getInitialState';
|
||||
|
||||
export const useTimeFormattedColumns = (datasourceId?: string) =>
|
||||
useSelector<ExplorePageState, string[]>(state =>
|
||||
datasourceId
|
||||
? state.explore.timeFormattedColumns?.[datasourceId] ?? []
|
||||
: [],
|
||||
);
|
||||
Reference in New Issue
Block a user