mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
fix(explore): timestamp format when copy datatable to clipboard (#17166)
* fix(explore): timestamp format when copy datatable to clipboard * Fix test * Fix test * Use translation for aria label * Memoize stringified cells * Fix test
This commit is contained in:
parent
e5a03423f9
commit
860e481a97
@ -27,10 +27,7 @@ import {
|
|||||||
SLOW_DEBOUNCE,
|
SLOW_DEBOUNCE,
|
||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import {
|
import { prepareCopyToClipboardTabularData } from 'src/utils/common';
|
||||||
applyFormattingToTabularData,
|
|
||||||
prepareCopyToClipboardTabularData,
|
|
||||||
} from 'src/utils/common';
|
|
||||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||||
import RowCountLabel from 'src/explore/components/RowCountLabel';
|
import RowCountLabel from 'src/explore/components/RowCountLabel';
|
||||||
|
|
||||||
@ -48,7 +45,7 @@ export const CopyButton = styled(Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const CopyNode = (
|
const CopyNode = (
|
||||||
<CopyButton buttonSize="xsmall">
|
<CopyButton buttonSize="xsmall" aria-label={t('Copy')}>
|
||||||
<i className="fa fa-clipboard" />
|
<i className="fa fa-clipboard" />
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
);
|
);
|
||||||
@ -103,18 +100,26 @@ export const RowCount = ({
|
|||||||
export const useFilteredTableData = (
|
export const useFilteredTableData = (
|
||||||
filterText: string,
|
filterText: string,
|
||||||
data?: Record<string, any>[],
|
data?: Record<string, any>[],
|
||||||
) =>
|
) => {
|
||||||
useMemo(() => {
|
const rowsAsStrings = useMemo(
|
||||||
|
() =>
|
||||||
|
data?.map((row: Record<string, any>) =>
|
||||||
|
Object.values(row).map(value => value?.toString().toLowerCase()),
|
||||||
|
) ?? [],
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
if (!data?.length) {
|
if (!data?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const formattedData = applyFormattingToTabularData(data);
|
return data.filter((_, index: number) =>
|
||||||
return formattedData.filter((row: Record<string, any>) =>
|
rowsAsStrings[index].some(value =>
|
||||||
Object.values(row).some(value =>
|
value?.includes(filterText.toLowerCase()),
|
||||||
value?.toString().toLowerCase().includes(filterText.toLowerCase()),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, [data, filterText]);
|
}, [data, filterText, rowsAsStrings]);
|
||||||
|
};
|
||||||
|
|
||||||
export const useTableColumns = (
|
export const useTableColumns = (
|
||||||
colnames?: string[],
|
colnames?: string[],
|
||||||
|
@ -17,17 +17,13 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import userEvent from '@testing-library/user-event';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
import * as copyUtils from 'src/utils/copy';
|
||||||
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import { DataTablesPane } from '.';
|
import { DataTablesPane } from '.';
|
||||||
|
|
||||||
fetchMock.post(
|
|
||||||
'http://api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
|
|
||||||
{ body: {} },
|
|
||||||
);
|
|
||||||
|
|
||||||
const createProps = () => ({
|
const createProps = () => ({
|
||||||
queryFormData: {
|
queryFormData: {
|
||||||
viz_type: 'heatmap',
|
viz_type: 'heatmap',
|
||||||
@ -65,10 +61,6 @@ const createProps = () => ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
fetchMock.done();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Rendering DataTablesPane correctly', () => {
|
test('Rendering DataTablesPane correctly', () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<DataTablesPane {...props} />, { useRedux: true });
|
render(<DataTablesPane {...props} />, { useRedux: true });
|
||||||
@ -108,3 +100,38 @@ test('Should show tabs: View samples', async () => {
|
|||||||
userEvent.click(await screen.findByText('View samples'));
|
userEvent.click(await screen.findByText('View samples'));
|
||||||
expect(await screen.findByText('0 rows retrieved')).toBeVisible();
|
expect(await screen.findByText('0 rows retrieved')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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' }] }],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
|
||||||
|
const props = createProps();
|
||||||
|
render(
|
||||||
|
<DataTablesPane
|
||||||
|
{...{
|
||||||
|
...props,
|
||||||
|
chartStatus: 'success',
|
||||||
|
queriesResponse: [
|
||||||
|
{
|
||||||
|
colnames: ['__timestamp', 'genre'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
userEvent.click(await screen.findByText('Data'));
|
||||||
|
expect(await screen.findByText('1 rows retrieved')).toBeVisible();
|
||||||
|
|
||||||
|
userEvent.click(screen.getByRole('button', { name: 'Copy' }));
|
||||||
|
expect(copyToClipboardSpy).toHaveBeenCalledWith(
|
||||||
|
'2009-01-01 00:00:00\tAction\n',
|
||||||
|
);
|
||||||
|
fetchMock.done();
|
||||||
|
});
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { JsonObject, styled, t } from '@superset-ui/core';
|
import { JsonObject, styled, t } from '@superset-ui/core';
|
||||||
import Collapse from 'src/components/Collapse';
|
import Collapse from 'src/components/Collapse';
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
@ -35,6 +35,7 @@ import {
|
|||||||
useFilteredTableData,
|
useFilteredTableData,
|
||||||
useTableColumns,
|
useTableColumns,
|
||||||
} from 'src/explore/components/DataTableControl';
|
} from 'src/explore/components/DataTableControl';
|
||||||
|
import { applyFormattingToTabularData } from 'src/utils/common';
|
||||||
|
|
||||||
const RESULT_TYPES = {
|
const RESULT_TYPES = {
|
||||||
results: 'results' as const,
|
results: 'results' as const,
|
||||||
@ -144,6 +145,18 @@ export const DataTablesPane = ({
|
|||||||
getFromLocalStorage(STORAGE_KEYS.isOpen, false),
|
getFromLocalStorage(STORAGE_KEYS.isOpen, false),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const formattedData = useMemo(
|
||||||
|
() => ({
|
||||||
|
[RESULT_TYPES.results]: applyFormattingToTabularData(
|
||||||
|
data[RESULT_TYPES.results],
|
||||||
|
),
|
||||||
|
[RESULT_TYPES.samples]: applyFormattingToTabularData(
|
||||||
|
data[RESULT_TYPES.samples],
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
const getData = useCallback(
|
const getData = useCallback(
|
||||||
(resultType: string) => {
|
(resultType: string) => {
|
||||||
setIsLoading(prevIsLoading => ({
|
setIsLoading(prevIsLoading => ({
|
||||||
@ -279,11 +292,11 @@ export const DataTablesPane = ({
|
|||||||
const filteredData = {
|
const filteredData = {
|
||||||
[RESULT_TYPES.results]: useFilteredTableData(
|
[RESULT_TYPES.results]: useFilteredTableData(
|
||||||
filterText,
|
filterText,
|
||||||
data[RESULT_TYPES.results],
|
formattedData[RESULT_TYPES.results],
|
||||||
),
|
),
|
||||||
[RESULT_TYPES.samples]: useFilteredTableData(
|
[RESULT_TYPES.samples]: useFilteredTableData(
|
||||||
filterText,
|
filterText,
|
||||||
data[RESULT_TYPES.samples],
|
formattedData[RESULT_TYPES.samples],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -334,7 +347,10 @@ export const DataTablesPane = ({
|
|||||||
const TableControls = (
|
const TableControls = (
|
||||||
<TableControlsWrapper>
|
<TableControlsWrapper>
|
||||||
<RowCount data={data[activeTabKey]} loading={isLoading[activeTabKey]} />
|
<RowCount data={data[activeTabKey]} loading={isLoading[activeTabKey]} />
|
||||||
<CopyToClipboardButton data={data[activeTabKey]} columns={columnNames} />
|
<CopyToClipboardButton
|
||||||
|
data={formattedData[activeTabKey]}
|
||||||
|
columns={columnNames}
|
||||||
|
/>
|
||||||
<FilterInput onChangeHandler={setFilterText} />
|
<FilterInput onChangeHandler={setFilterText} />
|
||||||
</TableControlsWrapper>
|
</TableControlsWrapper>
|
||||||
);
|
);
|
||||||
|
@ -55,7 +55,6 @@ describe('utils/common', () => {
|
|||||||
{ column1: 'dolor', column2: 'sit', column3: 'amet' },
|
{ column1: 'dolor', column2: 'sit', column3: 'amet' },
|
||||||
];
|
];
|
||||||
const column = ['column1', 'column2', 'column3'];
|
const column = ['column1', 'column2', 'column3'];
|
||||||
console.log(prepareCopyToClipboardTabularData(array, column));
|
|
||||||
expect(prepareCopyToClipboardTabularData(array, column)).toEqual(
|
expect(prepareCopyToClipboardTabularData(array, column)).toEqual(
|
||||||
'lorem\tipsum\t\ndolor\tsit\tamet\n',
|
'lorem\tipsum\t\ndolor\tsit\tamet\n',
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user