fix: copy to Clipboard order (#16299)

* copy to Clipboard order

* centralized copyToClipboard

* fixed table order

* fixed tests

* added colnames to all viz types

* added colnames to all viz types

* added colnames to all viz types
This commit is contained in:
AAfghahi 2021-08-24 17:47:09 -04:00 committed by GitHub
parent e71c6e60e4
commit 631ad02a76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 38 deletions

View File

@ -448,7 +448,7 @@ export default class ResultSet extends React.PureComponent<
if (this.props.cache && this.props.query.cached) { if (this.props.cache && this.props.query.cached) {
({ data } = this.state); ({ data } = this.state);
} }
const { columns } = this.props.query.results;
// Added compute logic to stop user from being able to Save & Explore // Added compute logic to stop user from being able to Save & Explore
const { const {
saveDatasetRadioBtnState, saveDatasetRadioBtnState,
@ -508,7 +508,7 @@ export default class ResultSet extends React.PureComponent<
)} )}
<CopyToClipboard <CopyToClipboard
text={prepareCopyToClipboardTabularData(data)} text={prepareCopyToClipboardTabularData(data, columns)}
wrapped={false} wrapped={false}
copyNode={ copyNode={
<Button buttonSize="small"> <Button buttonSize="small">

View File

@ -158,7 +158,6 @@ const TableView = ({
useSortBy, useSortBy,
usePagination, usePagination,
); );
useEffect(() => { useEffect(() => {
if (serverPagination && pageIndex !== initialState.pageIndex) { if (serverPagination && pageIndex !== initialState.pageIndex) {
onServerPagination({ onServerPagination({

View File

@ -55,11 +55,15 @@ const CopyNode = (
export const CopyToClipboardButton = ({ export const CopyToClipboardButton = ({
data, data,
columns,
}: { }: {
data?: Record<string, any>; data?: Record<string, any>;
columns?: string[];
}) => ( }) => (
<CopyToClipboard <CopyToClipboard
text={data ? prepareCopyToClipboardTabularData(data) : ''} text={
data && columns ? prepareCopyToClipboardTabularData(data, columns) : ''
}
wrapped={false} wrapped={false}
copyNode={CopyNode} copyNode={CopyNode}
/> />
@ -113,29 +117,32 @@ export const useFilteredTableData = (
}, [data, filterText]); }, [data, filterText]);
export const useTableColumns = ( export const useTableColumns = (
colnames?: string[],
data?: Record<string, any>[], data?: Record<string, any>[],
moreConfigs?: { [key: string]: Partial<Column> }, moreConfigs?: { [key: string]: Partial<Column> },
) => ) =>
useMemo( useMemo(
() => () =>
data?.length colnames && data?.length
? Object.keys(data[0]).map( ? colnames
key => .filter((column: string) => Object.keys(data[0]).includes(column))
({ .map(
accessor: row => row[key], key =>
Header: key, ({
Cell: ({ value }) => { accessor: row => row[key],
if (value === true) { Header: key,
return BOOL_TRUE_DISPLAY; Cell: ({ value }) => {
} if (value === true) {
if (value === false) { return BOOL_TRUE_DISPLAY;
return BOOL_FALSE_DISPLAY; }
} if (value === false) {
return String(value); return BOOL_FALSE_DISPLAY;
}, }
...moreConfigs?.[key], return String(value);
} as Column), },
) ...moreConfigs?.[key],
} as Column),
)
: [], : [],
[data, moreConfigs], [data, moreConfigs],
); );

View File

@ -42,9 +42,10 @@ const data = [
[unicodeKey]: unicodeKey, [unicodeKey]: unicodeKey,
}, },
]; ];
const all_columns = ['col01', 'col02', 'col03', asciiKey, unicodeKey];
test('useTableColumns with no options', () => { test('useTableColumns with no options', () => {
const hook = renderHook(() => useTableColumns(data)); const hook = renderHook(() => useTableColumns(all_columns, data));
expect(hook.result.current).toEqual([ expect(hook.result.current).toEqual([
{ {
Cell: expect.any(Function), Cell: expect.any(Function),
@ -83,7 +84,7 @@ test('useTableColumns with no options', () => {
test('use only the first record columns', () => { test('use only the first record columns', () => {
const newData = [data[3], data[0]]; const newData = [data[3], data[0]];
const hook = renderHook(() => useTableColumns(newData)); const hook = renderHook(() => useTableColumns(all_columns, newData));
expect(hook.result.current).toEqual([ expect(hook.result.current).toEqual([
{ {
Cell: expect.any(Function), Cell: expect.any(Function),
@ -134,7 +135,9 @@ test('use only the first record columns', () => {
}); });
test('useTableColumns with options', () => { test('useTableColumns with options', () => {
const hook = renderHook(() => useTableColumns(data, { col01: { id: 'ID' } })); const hook = renderHook(() =>
useTableColumns(all_columns, data, { col01: { id: 'ID' } }),
);
expect(hook.result.current).toEqual([ expect(hook.result.current).toEqual([
{ {
Cell: expect.any(Function), Cell: expect.any(Function),

View File

@ -58,6 +58,11 @@ const createProps = () => ({
tableSectionHeight: 156.9, tableSectionHeight: 156.9,
chartStatus: 'rendered', chartStatus: 'rendered',
onCollapseChange: jest.fn(), onCollapseChange: jest.fn(),
queriesResponse: [
{
colnames: [],
},
],
}); });
afterAll(() => { afterAll(() => {

View File

@ -112,6 +112,7 @@ export const DataTablesPane = ({
chartStatus, chartStatus,
ownState, ownState,
errorMessage, errorMessage,
queriesResponse,
}: { }: {
queryFormData: Record<string, any>; queryFormData: Record<string, any>;
tableSectionHeight: number; tableSectionHeight: number;
@ -119,6 +120,7 @@ export const DataTablesPane = ({
ownState?: JsonObject; ownState?: JsonObject;
onCollapseChange: (openPanelName: string) => void; onCollapseChange: (openPanelName: string) => void;
errorMessage?: JSX.Element; errorMessage?: JSX.Element;
queriesResponse: Record<string, any>;
}) => { }) => {
const [data, setData] = useState<{ const [data, setData] = useState<{
[RESULT_TYPES.results]?: Record<string, any>[]; [RESULT_TYPES.results]?: Record<string, any>[];
@ -128,6 +130,7 @@ export const DataTablesPane = ({
[RESULT_TYPES.results]: true, [RESULT_TYPES.results]: true,
[RESULT_TYPES.samples]: true, [RESULT_TYPES.samples]: true,
}); });
const [columnNames, setColumnNames] = useState<string[]>([]);
const [error, setError] = useState(NULLISH_RESULTS_STATE); const [error, setError] = useState(NULLISH_RESULTS_STATE);
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
const [activeTabKey, setActiveTabKey] = useState<string>( const [activeTabKey, setActiveTabKey] = useState<string>(
@ -220,6 +223,13 @@ export const DataTablesPane = ({
})); }));
}, [queryFormData.adhoc_filters, queryFormData.datasource]); }, [queryFormData.adhoc_filters, queryFormData.datasource]);
useEffect(() => {
if (queriesResponse) {
const { colnames } = queriesResponse[0];
setColumnNames([...colnames]);
}
}, [queriesResponse]);
useEffect(() => { useEffect(() => {
if (panelOpen && isRequestPending[RESULT_TYPES.results]) { if (panelOpen && isRequestPending[RESULT_TYPES.results]) {
if (errorMessage) { if (errorMessage) {
@ -277,9 +287,17 @@ export const DataTablesPane = ({
), ),
}; };
// 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 = { const columns = {
[RESULT_TYPES.results]: useTableColumns(data[RESULT_TYPES.results]), [RESULT_TYPES.results]: useTableColumns(
[RESULT_TYPES.samples]: useTableColumns(data[RESULT_TYPES.samples]), columnNames,
data[RESULT_TYPES.results],
),
[RESULT_TYPES.samples]: useTableColumns(
columnNames,
data[RESULT_TYPES.samples],
),
}; };
const renderDataTable = (type: string) => { const renderDataTable = (type: string) => {
@ -316,7 +334,7 @@ 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]} /> <CopyToClipboardButton data={data[activeTabKey]} columns={columnNames} />
<FilterInput onChangeHandler={setFilterText} /> <FilterInput onChangeHandler={setFilterText} />
</TableControlsWrapper> </TableControlsWrapper>
); );

View File

@ -116,7 +116,6 @@ const ExploreChartPanel = props => {
const theme = useTheme(); const theme = useTheme();
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR; const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR; const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
const { height: hHeight, ref: headerRef } = useResizeDetector({ const { height: hHeight, ref: headerRef } = useResizeDetector({
refreshMode: 'debounce', refreshMode: 'debounce',
refreshRate: 300, refreshRate: 300,
@ -128,7 +127,6 @@ const ExploreChartPanel = props => {
const [splitSizes, setSplitSizes] = useState( const [splitSizes, setSplitSizes] = useState(
getFromLocalStorage(STORAGE_KEYS.sizes, INITIAL_SIZES), getFromLocalStorage(STORAGE_KEYS.sizes, INITIAL_SIZES),
); );
const { slice } = props; const { slice } = props;
const updateQueryContext = useCallback( const updateQueryContext = useCallback(
async function fetchChartData() { async function fetchChartData() {
@ -211,7 +209,6 @@ const ExploreChartPanel = props => {
} }
setSplitSizes(splitSizes); setSplitSizes(splitSizes);
}; };
const renderChart = useCallback(() => { const renderChart = useCallback(() => {
const { chart, vizType } = props; const { chart, vizType } = props;
const newHeight = const newHeight =
@ -317,6 +314,7 @@ const ExploreChartPanel = props => {
onCollapseChange={onCollapseChange} onCollapseChange={onCollapseChange}
chartStatus={props.chart.chartStatus} chartStatus={props.chart.chartStatus}
errorMessage={props.errorMessage} errorMessage={props.errorMessage}
queriesResponse={props.chart.queriesResponse}
/> />
</Split> </Split>
)} )}

View File

@ -87,10 +87,21 @@ export function optionFromValue(opt) {
return { value: optionValue(opt), label: optionLabel(opt) }; return { value: optionValue(opt), label: optionLabel(opt) };
} }
export function prepareCopyToClipboardTabularData(data) { export function prepareCopyToClipboardTabularData(data, columns) {
let result = ''; let result = '';
for (let i = 0; i < data.length; i += 1) { for (let i = 0; i < data.length; i += 1) {
result += `${Object.values(data[i]).join('\t')}\n`; const row = {};
for (let j = 0; j < columns.length; j += 1) {
// JavaScript does not mantain the order of a mixed set of keys (i.e integers and strings)
// the below function orders the keys based on the column names.
const key = columns[j].name || columns[j];
if (data[i][key]) {
row[j] = data[i][key];
} else {
row[j] = data[i][parseFloat(key)];
}
}
result += `${Object.values(row).join('\t')}\n`;
} }
return result; return result;
} }

View File

@ -46,15 +46,18 @@ describe('utils/common', () => {
describe('prepareCopyToClipboardTabularData', () => { describe('prepareCopyToClipboardTabularData', () => {
it('converts empty array', () => { it('converts empty array', () => {
const array = []; const array = [];
expect(prepareCopyToClipboardTabularData(array)).toEqual(''); const column = [];
expect(prepareCopyToClipboardTabularData(array, column)).toEqual('');
}); });
it('converts non empty array', () => { it('converts non empty array', () => {
const array = [ const array = [
{ column1: 'lorem', column2: 'ipsum' }, { column1: 'lorem', column2: 'ipsum' },
{ column1: 'dolor', column2: 'sit', column3: 'amet' }, { column1: 'dolor', column2: 'sit', column3: 'amet' },
]; ];
expect(prepareCopyToClipboardTabularData(array)).toEqual( const column = ['column1', 'column2', 'column3'];
'lorem\tipsum\ndolor\tsit\tamet\n', console.log(prepareCopyToClipboardTabularData(array, column));
expect(prepareCopyToClipboardTabularData(array, column)).toEqual(
'lorem\tipsum\t\ndolor\tsit\tamet\n',
); );
}); });
}); });

View File

@ -450,7 +450,8 @@ class BaseViz:
payload = self.get_df_payload(query_obj) payload = self.get_df_payload(query_obj)
df = payload.get("df") # if payload does not have a df, we are raising an error here.
df = cast(Optional[pd.DataFrame], payload["df"])
if self.status != utils.QueryStatus.FAILED: if self.status != utils.QueryStatus.FAILED:
payload["data"] = self.get_data(df) payload["data"] = self.get_data(df)
@ -482,7 +483,8 @@ class BaseViz:
for col in filter_columns for col in filter_columns
if col not in columns and col not in filter_values_columns if col not in columns and col not in filter_values_columns
] + rejected_time_columns ] + rejected_time_columns
if df is not None:
payload["colnames"] = list(df.columns)
return payload return payload
def get_df_payload( def get_df_payload(