feat: multiple results pane on explore and dashboard (#20277)

This commit is contained in:
Yongjie Zhao
2022-06-09 09:11:34 +08:00
committed by GitHub
parent 1e5cacda8f
commit fd129873ce
14 changed files with 678 additions and 213 deletions

View File

@@ -36,7 +36,7 @@ import Icons from 'src/components/Icons';
import ModalTrigger from 'src/components/ModalTrigger';
import Button from 'src/components/Button';
import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal';
import { ResultsPane } from 'src/explore/components/DataTablesPane';
import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane';
const MENU_KEYS = {
CROSS_FILTER_SCOPING: 'cross_filter_scoping',
@@ -340,11 +340,12 @@ class SliceHeaderControls extends React.PureComponent<
}
modalTitle={t('Chart Data: %s', slice.slice_name)}
modalBody={
<ResultsPane
<ResultsPaneOnDashboard
queryFormData={this.props.formData}
queryForce={false}
dataSize={20}
isRequest
isVisible
/>
}
modalFooter={

View File

@@ -163,6 +163,7 @@ const DataTableTemporalHeaderCell = ({
columnName,
onTimeColumnChange,
datasourceId,
isOriginalTimeColumn,
}: {
columnName: string;
onTimeColumnChange: (
@@ -170,15 +171,12 @@ const DataTableTemporalHeaderCell = ({
columnType: FormatPickerValue,
) => void;
datasourceId?: string;
isOriginalTimeColumn: boolean;
}) => {
const theme = useTheme();
const [isOriginalTimeColumn, setIsOriginalTimeColumn] = useState<boolean>(
getTimeColumns(datasourceId).includes(columnName),
);
const onChange = (e: any) => {
onTimeColumnChange(columnName, e.target.value);
setIsOriginalTimeColumn(getTimeColumns(datasourceId).includes(columnName));
};
const overlayContent = useMemo(
@@ -313,6 +311,8 @@ export const useTableColumns = (
colType === GenericDataType.TEMPORAL
? originalFormattedTimeColumns.indexOf(key)
: -1;
const isOriginalTimeColumn =
originalFormattedTimeColumns.includes(key);
return {
id: key,
accessor: row => row[key],
@@ -324,6 +324,7 @@ export const useTableColumns = (
columnName={key}
datasourceId={datasourceId}
onTimeColumnChange={onTimeColumnChange}
isOriginalTimeColumn={isOriginalTimeColumn}
/>
) : (
key

View File

@@ -31,13 +31,12 @@ import {
setItem,
LocalStorageKeys,
} from 'src/utils/localStorageHelpers';
import { ResultsPane, SamplesPane, TableControlsWrapper } from './components';
import { DataTablesPaneProps } from './types';
enum ResultTypes {
Results = 'results',
Samples = 'samples',
}
import {
SamplesPane,
TableControlsWrapper,
useResultsPane,
} from './components';
import { DataTablesPaneProps, ResultTypes } from './types';
const SouthPane = styled.div`
${({ theme }) => `
@@ -114,7 +113,7 @@ export const DataTablesPane = ({
if (
panelOpen &&
activeTabKey === ResultTypes.Results &&
activeTabKey.startsWith(ResultTypes.Results) &&
chartStatus === 'rendered'
) {
setIsRequest({
@@ -187,6 +186,35 @@ export const DataTablesPane = ({
);
}, [handleCollapseChange, panelOpen, theme.colors.grayscale.base]);
const queryResultsPanes = useResultsPane({
errorMessage,
queryFormData,
queryForce,
ownState,
isRequest: isRequest.results,
actions,
isVisible: ResultTypes.Results === activeTabKey,
}).map((pane, idx) => {
if (idx === 0) {
return (
<Tabs.TabPane tab={t('Results')} key={ResultTypes.Results}>
{pane}
</Tabs.TabPane>
);
}
if (idx > 0) {
return (
<Tabs.TabPane
tab={t('Results %s', idx + 1)}
key={`${ResultTypes.Results} ${idx + 1}`}
>
{pane}
</Tabs.TabPane>
);
}
return null;
});
return (
<SouthPane data-test="some-purposeful-instance">
<Tabs
@@ -195,22 +223,14 @@ export const DataTablesPane = ({
activeKey={panelOpen ? activeTabKey : ''}
onTabClick={handleTabClick}
>
<Tabs.TabPane tab={t('Results')} key={ResultTypes.Results}>
<ResultsPane
errorMessage={errorMessage}
queryFormData={queryFormData}
queryForce={queryForce}
ownState={ownState}
isRequest={isRequest.results}
actions={actions}
/>
</Tabs.TabPane>
{queryResultsPanes}
<Tabs.TabPane tab={t('Samples')} key={ResultTypes.Samples}>
<SamplesPane
datasource={datasource}
queryForce={queryForce}
isRequest={isRequest.samples}
actions={actions}
isVisible={ResultTypes.Samples === activeTabKey}
/>
</Tabs.TabPane>
</Tabs>

View File

@@ -0,0 +1,69 @@
/**
* 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 React from 'react';
import { t } from '@superset-ui/core';
import Tabs from 'src/components/Tabs';
import { ResultTypes, ResultsPaneProps } from '../types';
import { useResultsPane } from './useResultsPane';
export const ResultsPaneOnDashboard = ({
isRequest,
queryFormData,
queryForce,
ownState,
errorMessage,
actions,
isVisible,
dataSize = 50,
}: ResultsPaneProps) => {
const resultsPanes = useResultsPane({
errorMessage,
queryFormData,
queryForce,
ownState,
isRequest,
actions,
dataSize,
isVisible,
});
if (resultsPanes.length === 1) {
return resultsPanes[0];
}
const panes = resultsPanes.map((pane, idx) => {
if (idx === 0) {
return (
<Tabs.TabPane tab={t('Results')} key={ResultTypes.Results}>
{pane}
</Tabs.TabPane>
);
}
return (
<Tabs.TabPane
tab={t('Results %s', idx + 1)}
key={`${ResultTypes.Results} ${idx + 1}`}
>
{pane}
</Tabs.TabPane>
);
});
return <Tabs fullWidth={false}> {panes} </Tabs>;
};

View File

@@ -41,6 +41,7 @@ export const SamplesPane = ({
queryForce,
actions,
dataSize = 50,
isVisible,
}: SamplesPaneProps) => {
const [filterText, setFilterText] = useState('');
const [data, setData] = useState<Record<string, any>[][]>([]);
@@ -90,7 +91,7 @@ export const SamplesPane = ({
coltypes,
data,
datasourceId,
isRequest,
isVisible,
);
const filteredData = useFilteredTableData(filterText, data);

View File

@@ -0,0 +1,73 @@
/**
* 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 React, { useState } from 'react';
import { t } from '@superset-ui/core';
import TableView, { EmptyWrapperType } from 'src/components/TableView';
import {
useFilteredTableData,
useTableColumns,
} from 'src/explore/components/DataTableControl';
import { TableControls } from './DataTableControls';
import { SingleQueryResultPaneProp } from '../types';
export const SingleQueryResultPane = ({
data,
colnames,
coltypes,
datasourceId,
dataSize = 50,
isVisible,
}: SingleQueryResultPaneProp) => {
const [filterText, setFilterText] = useState('');
// 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(
colnames,
coltypes,
data,
datasourceId,
isVisible,
);
const filteredData = useFilteredTableData(filterText, data);
return (
<>
<TableControls
data={filteredData}
columnNames={colnames}
columnTypes={coltypes}
datasourceId={datasourceId}
onInputChange={input => setFilterText(input)}
isLoading={false}
/>
<TableView
columns={columns}
data={filteredData}
pageSize={dataSize}
noDataText={t('No results')}
emptyWrapperType={EmptyWrapperType.Small}
className="table-condensed"
isPaginationSticky
showRowCount={false}
small
/>
</>
);
};

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
export { ResultsPane } from './ResultsPane';
export { ResultsPaneOnDashboard } from './ResultsPaneOnDashboard';
export { SamplesPane } from './SamplesPane';
export { TableControls, TableControlsWrapper } from './DataTableControls';
export { useResultsPane } from './useResultsPane';

View File

@@ -17,18 +17,15 @@
* under the License.
*/
import React, { useState, useEffect } from 'react';
import { ensureIsArray, GenericDataType, styled, t } from '@superset-ui/core';
import { ensureIsArray, styled, t } from '@superset-ui/core';
import Loading from 'src/components/Loading';
import { EmptyStateMedium } from 'src/components/EmptyState';
import TableView, { EmptyWrapperType } from 'src/components/TableView';
import {
useFilteredTableData,
useTableColumns,
} from 'src/explore/components/DataTableControl';
import { getChartDataRequest } from 'src/components/Chart/chartAction';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { ResultsPaneProps, QueryResultInterface } from '../types';
import { getQueryCount } from '../utils';
import { SingleQueryResultPane } from './SingleQueryResultPane';
import { TableControls } from './DataTableControls';
import { ResultsPaneProps } from '../types';
const Error = styled.pre`
margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`};
@@ -36,21 +33,22 @@ const Error = styled.pre`
const cache = new WeakSet();
export const ResultsPane = ({
export const useResultsPane = ({
isRequest,
queryFormData,
queryForce,
ownState,
errorMessage,
actions,
isVisible,
dataSize = 50,
}: ResultsPaneProps) => {
const [filterText, setFilterText] = useState('');
const [data, setData] = useState<Record<string, any>[][]>([]);
const [colnames, setColnames] = useState<string[]>([]);
const [coltypes, setColtypes] = useState<GenericDataType[]>([]);
}: ResultsPaneProps): React.ReactElement[] => {
const [resultResp, setResultResp] = useState<QueryResultInterface[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [responseError, setResponseError] = useState<string>('');
const queryCount = getQueryCount(
queryFormData?.viz_type || queryFormData?.vizType,
);
useEffect(() => {
// it's an invalid formData when gets a errorMessage
@@ -65,28 +63,7 @@ export const ResultsPane = ({
ownState,
})
.then(({ json }) => {
const { colnames, coltypes } = json.result[0];
// Only displaying the first query is currently supported
if (json.result.length > 1) {
// todo: move these code to the backend, shouldn't loop by row in FE
const data: any[] = [];
json.result.forEach((item: { data: any[] }) => {
item.data.forEach((row, i) => {
if (data[i] !== undefined) {
data[i] = { ...data[i], ...row };
} else {
data[i] = row;
}
});
});
setData(data);
setColnames(colnames);
setColtypes(coltypes);
} else {
setData(ensureIsArray(json.result[0].data));
setColnames(colnames);
setColtypes(coltypes);
}
setResultResp(ensureIsArray(json.result));
setResponseError('');
cache.add(queryFormData);
if (queryForce && actions) {
@@ -110,68 +87,50 @@ export const ResultsPane = ({
}
}, [errorMessage]);
// 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(
colnames,
coltypes,
data,
queryFormData.datasource,
isRequest,
);
const filteredData = useFilteredTableData(filterText, data);
if (isLoading) {
return <Loading />;
return Array(queryCount).fill(<Loading />);
}
if (errorMessage) {
const title = t('Run a query to display results');
return <EmptyStateMedium image="document.svg" title={title} />;
return Array(queryCount).fill(
<EmptyStateMedium image="document.svg" title={title} />,
);
}
if (responseError) {
return (
const err = (
<>
<TableControls
data={filteredData}
columnNames={colnames}
columnTypes={coltypes}
datasourceId={queryFormData?.datasource}
onInputChange={input => setFilterText(input)}
isLoading={isLoading}
data={[]}
columnNames={[]}
columnTypes={[]}
datasourceId={queryFormData.datasource}
onInputChange={() => {}}
isLoading={false}
/>
<Error>{responseError}</Error>
</>
);
return Array(queryCount).fill(err);
}
if (data.length === 0) {
if (resultResp.length === 0) {
const title = t('No results were returned for this query');
return <EmptyStateMedium image="document.svg" title={title} />;
return Array(queryCount).fill(
<EmptyStateMedium image="document.svg" title={title} />,
);
}
return (
<>
<TableControls
data={filteredData}
columnNames={colnames}
columnTypes={coltypes}
datasourceId={queryFormData?.datasource}
onInputChange={input => setFilterText(input)}
isLoading={isLoading}
/>
<TableView
columns={columns}
data={filteredData}
pageSize={dataSize}
noDataText={t('No results')}
emptyWrapperType={EmptyWrapperType.Small}
className="table-condensed"
isPaginationSticky
showRowCount={false}
small
/>
</>
);
return resultResp.map((result, idx) => (
<SingleQueryResultPane
data={result.data}
colnames={result.colnames}
coltypes={result.coltypes}
dataSize={dataSize}
datasourceId={queryFormData.datasource}
key={idx}
isVisible={isVisible}
/>
));
};

View File

@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
@@ -26,56 +25,8 @@ import {
screen,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { DatasourceType } from '@superset-ui/core';
import { exploreActions } from 'src/explore/actions/exploreActions';
import { ChartStatus } from 'src/explore/types';
import { DataTablesPane } from '.';
const createProps = () => ({
queryFormData: {
viz_type: 'heatmap',
datasource: '34__table',
slice_id: 456,
url_params: {},
time_range: 'Last week',
all_columns_x: 'source',
all_columns_y: 'target',
metric: 'sum__value',
adhoc_filters: [],
row_limit: 10000,
linear_color_scheme: 'blue_white_yellow',
xscale_interval: null,
yscale_interval: null,
canvas_image_rendering: 'pixelated',
normalize_across: 'heatmap',
left_margin: 'auto',
bottom_margin: 'auto',
y_axis_bounds: [null, null],
y_axis_format: 'SMART_NUMBER',
show_perc: true,
sort_x_axis: 'alpha_asc',
sort_y_axis: 'alpha_asc',
extra_form_data: {},
},
queryForce: false,
chartStatus: 'rendered' as ChartStatus,
onCollapseChange: jest.fn(),
queriesResponse: [
{
colnames: [],
},
],
datasource: {
id: 0,
name: '',
type: DatasourceType.Table,
columns: [],
metrics: [],
columnFormats: {},
verboseMap: {},
},
actions: exploreActions,
});
import { DataTablesPane } from '..';
import { createDataTablesPaneProps } from './fixture';
describe('DataTablesPane', () => {
// Collapsed/expanded state depends on local storage
@@ -89,7 +40,7 @@ describe('DataTablesPane', () => {
});
test('Rendering DataTablesPane correctly', () => {
const props = createProps();
const props = createDataTablesPaneProps(0);
render(<DataTablesPane {...props} />, { useRedux: true });
expect(screen.getByText('Results')).toBeVisible();
expect(screen.getByText('Samples')).toBeVisible();
@@ -97,7 +48,7 @@ describe('DataTablesPane', () => {
});
test('Collapse/Expand buttons', async () => {
const props = createProps();
const props = createDataTablesPaneProps(0);
render(<DataTablesPane {...props} />, {
useRedux: true,
});
@@ -112,7 +63,7 @@ describe('DataTablesPane', () => {
});
test('Should show tabs: View results', async () => {
const props = createProps();
const props = createDataTablesPaneProps(0);
render(<DataTablesPane {...props} />, {
useRedux: true,
});
@@ -121,9 +72,8 @@ describe('DataTablesPane', () => {
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
localStorage.clear();
});
test('Should show tabs: View samples', async () => {
const props = createProps();
const props = createDataTablesPaneProps(0);
render(<DataTablesPane {...props} />, {
useRedux: true,
});
@@ -146,31 +96,10 @@ describe('DataTablesPane', () => {
},
);
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
const props = createProps();
render(
<DataTablesPane
{...{
...props,
chartStatus: 'rendered',
queriesResponse: [
{
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
}}
/>,
{
useRedux: true,
initialState: {
explore: {
originalFormattedTimeColumns: {
'34__table': ['__timestamp'],
},
},
},
},
);
const props = createDataTablesPaneProps(456);
render(<DataTablesPane {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByText('Results'));
expect(await screen.findByText('1 row')).toBeVisible();
@@ -184,7 +113,7 @@ describe('DataTablesPane', () => {
test('Search table', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A789%7D',
{
result: [
{
@@ -198,31 +127,10 @@ describe('DataTablesPane', () => {
],
},
);
const props = createProps();
render(
<DataTablesPane
{...{
...props,
chartStatus: 'rendered',
queriesResponse: [
{
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
}}
/>,
{
useRedux: true,
initialState: {
explore: {
originalFormattedTimeColumns: {
'34__table': ['__timestamp'],
},
},
},
},
);
const props = createDataTablesPaneProps(789);
render(<DataTablesPane {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByText('Results'));
expect(await screen.findByText('2 rows')).toBeVisible();
expect(screen.getByText('Action')).toBeVisible();

View File

@@ -0,0 +1,160 @@
/**
* 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 React from 'react';
import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import {
render,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { exploreActions } from 'src/explore/actions/exploreActions';
import { promiseTimeout } from '@superset-ui/core';
import { ResultsPaneOnDashboard } from '../components';
import { createResultsPaneOnDashboardProps } from './fixture';
describe('ResultsPaneOnDashboard', () => {
// render and render errorMessage
fetchMock.post(
'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A121%7D',
{
result: [],
},
);
// force query, render and search
fetchMock.post(
'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A144%7D&force=true',
{
result: [
{
data: [
{ __timestamp: 1230768000000, genre: 'Action' },
{ __timestamp: 1230768000010, genre: 'Horror' },
],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
},
);
// error response
fetchMock.post(
'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A169%7D',
400,
);
// multiple results pane
fetchMock.post(
'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A196%7D',
{
result: [
{
data: [
{ __timestamp: 1230768000000 },
{ __timestamp: 1230768000010 },
],
colnames: ['__timestamp'],
coltypes: [2],
},
{
data: [{ genre: 'Action' }, { genre: 'Horror' }],
colnames: ['genre'],
coltypes: [1],
},
],
},
);
const setForceQuery = jest.spyOn(exploreActions, 'setForceQuery');
afterAll(() => {
fetchMock.reset();
jest.resetAllMocks();
});
test('render', async () => {
const props = createResultsPaneOnDashboardProps({ sliceId: 121 });
const { findByText } = render(<ResultsPaneOnDashboard {...props} />, {
useRedux: true,
});
expect(
await findByText('No results were returned for this query'),
).toBeVisible();
});
test('render errorMessage', async () => {
const props = createResultsPaneOnDashboardProps({
sliceId: 121,
errorMessage: <p>error</p>,
});
const { findByText } = render(<ResultsPaneOnDashboard {...props} />, {
useRedux: true,
});
expect(await findByText('Run a query to display results')).toBeVisible();
});
test('error response', async () => {
const props = createResultsPaneOnDashboardProps({
sliceId: 169,
});
const { findByText } = render(<ResultsPaneOnDashboard {...props} />, {
useRedux: true,
});
expect(await findByText('0 rows')).toBeVisible();
expect(await findByText('Bad Request')).toBeVisible();
});
test('force query, render and search', async () => {
const props = createResultsPaneOnDashboardProps({
sliceId: 144,
queryForce: true,
});
const { queryByText, getByPlaceholderText } = render(
<ResultsPaneOnDashboard {...props} />,
{
useRedux: true,
},
);
await promiseTimeout(() => {
expect(setForceQuery).toHaveBeenCalledTimes(1);
}, 10);
expect(queryByText('2 rows')).toBeVisible();
expect(queryByText('Action')).toBeVisible();
expect(queryByText('Horror')).toBeVisible();
userEvent.type(getByPlaceholderText('Search'), 'hor');
await waitForElementToBeRemoved(() => queryByText('Action'));
expect(queryByText('Horror')).toBeVisible();
expect(queryByText('Action')).not.toBeInTheDocument();
});
test('multiple results pane', async () => {
const props = createResultsPaneOnDashboardProps({
sliceId: 196,
vizType: 'mixed_timeseries',
});
const { findByText } = render(<ResultsPaneOnDashboard {...props} />, {
useRedux: true,
});
expect(await findByText('Results')).toBeVisible();
expect(await findByText('Results 2')).toBeVisible();
});
});

View File

@@ -0,0 +1,106 @@
/**
* 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 React from 'react';
import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import {
render,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { exploreActions } from 'src/explore/actions/exploreActions';
import { promiseTimeout } from '@superset-ui/core';
import { SamplesPane } from '../components';
import { createSamplesPaneProps } from './fixture';
describe('SamplesPane', () => {
fetchMock.get('end:/api/v1/dataset/34/samples?force=false', {
result: {
data: [],
colnames: [],
coltypes: [],
},
});
fetchMock.get('end:/api/v1/dataset/35/samples?force=true', {
result: {
data: [
{ __timestamp: 1230768000000, genre: 'Action' },
{ __timestamp: 1230768000010, genre: 'Horror' },
],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
});
fetchMock.get('end:/api/v1/dataset/36/samples?force=false', 400);
const setForceQuery = jest.spyOn(exploreActions, 'setForceQuery');
afterAll(() => {
fetchMock.reset();
jest.resetAllMocks();
});
test('render', async () => {
const props = createSamplesPaneProps({ datasourceId: 34 });
const { findByText } = render(<SamplesPane {...props} />);
expect(
await findByText('No samples were returned for this dataset'),
).toBeVisible();
await promiseTimeout(() => {
expect(setForceQuery).toHaveBeenCalledTimes(0);
}, 10);
});
test('error response', async () => {
const props = createSamplesPaneProps({
datasourceId: 36,
});
const { findByText } = render(<SamplesPane {...props} />, {
useRedux: true,
});
expect(await findByText('Error: Bad Request')).toBeVisible();
});
test('force query, render and search', async () => {
const props = createSamplesPaneProps({
datasourceId: 35,
queryForce: true,
});
const { queryByText, getByPlaceholderText } = render(
<SamplesPane {...props} />,
{
useRedux: true,
},
);
await promiseTimeout(() => {
expect(setForceQuery).toHaveBeenCalledTimes(1);
}, 10);
expect(queryByText('2 rows')).toBeVisible();
expect(queryByText('Action')).toBeVisible();
expect(queryByText('Horror')).toBeVisible();
userEvent.type(getByPlaceholderText('Search'), 'hor');
await waitForElementToBeRemoved(() => queryByText('Action'));
expect(queryByText('Horror')).toBeVisible();
expect(queryByText('Action')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,119 @@
/**
* 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 React from 'react';
import { DatasourceType } from '@superset-ui/core';
import { exploreActions } from 'src/explore/actions/exploreActions';
import { ChartStatus } from 'src/explore/types';
import {
DataTablesPaneProps,
SamplesPaneProps,
ResultsPaneProps,
} from '../types';
const queryFormData = {
viz_type: 'heatmap',
datasource: '34__table',
slice_id: 456,
url_params: {},
time_range: 'Last week',
all_columns_x: 'source',
all_columns_y: 'target',
metric: 'sum__value',
adhoc_filters: [],
row_limit: 10000,
linear_color_scheme: 'blue_white_yellow',
xscale_interval: null,
yscale_interval: null,
canvas_image_rendering: 'pixelated',
normalize_across: 'heatmap',
left_margin: 'auto',
bottom_margin: 'auto',
y_axis_bounds: [null, null],
y_axis_format: 'SMART_NUMBER',
show_perc: true,
sort_x_axis: 'alpha_asc',
sort_y_axis: 'alpha_asc',
extra_form_data: {},
};
const datasource = {
id: 34,
name: '',
type: DatasourceType.Table,
columns: [],
metrics: [],
columnFormats: {},
verboseMap: {},
};
export const createDataTablesPaneProps = (sliceId: number) =>
({
queryFormData: {
...queryFormData,
slice_id: sliceId,
},
datasource,
queryForce: false,
chartStatus: 'rendered' as ChartStatus,
onCollapseChange: jest.fn(),
actions: exploreActions,
} as DataTablesPaneProps);
export const createSamplesPaneProps = ({
datasourceId,
queryForce = false,
isRequest = true,
}: {
datasourceId: number;
queryForce?: boolean;
isRequest?: boolean;
}) =>
({
isRequest,
datasource: { ...datasource, id: datasourceId },
queryForce,
isVisible: true,
actions: exploreActions,
} as SamplesPaneProps);
export const createResultsPaneOnDashboardProps = ({
sliceId,
errorMessage,
vizType = 'table',
queryForce = false,
isRequest = true,
}: {
sliceId: number;
vizType?: string;
errorMessage?: React.ReactElement;
queryForce?: boolean;
isRequest?: boolean;
}) =>
({
isRequest,
queryFormData: {
...queryFormData,
slice_id: sliceId,
viz_type: vizType,
},
queryForce,
isVisible: true,
actions: exploreActions,
errorMessage,
} as ResultsPaneProps);

View File

@@ -25,6 +25,11 @@ import {
import { ExploreActions } from 'src/explore/actions/exploreActions';
import { ChartStatus } from 'src/explore/types';
export enum ResultTypes {
Results = 'results',
Samples = 'samples',
}
export interface DataTablesPaneProps {
queryFormData: QueryFormData;
datasource: Datasource;
@@ -44,6 +49,8 @@ export interface ResultsPaneProps {
errorMessage?: React.ReactElement;
actions?: ExploreActions;
dataSize?: number;
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
isVisible: boolean;
}
export interface SamplesPaneProps {
@@ -52,6 +59,8 @@ export interface SamplesPaneProps {
queryForce: boolean;
actions?: ExploreActions;
dataSize?: number;
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
isVisible: boolean;
}
export interface TableControlsProps {
@@ -63,3 +72,17 @@ export interface TableControlsProps {
columnTypes: GenericDataType[];
isLoading: boolean;
}
export interface QueryResultInterface {
colnames: string[];
coltypes: GenericDataType[];
data: Record<string, any>[][];
}
export interface SingleQueryResultPaneProp extends QueryResultInterface {
// {datasource.id}__{datasource.type}, eg: 1__table
datasourceId: string;
dataSize?: number;
// reload OriginalFormattedTimeColumns from localStorage when isVisible is true
isVisible: boolean;
}

View File

@@ -0,0 +1,24 @@
/**
* 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.
*/
const queryObjectCount = {
mixed_timeseries: 2,
};
export const getQueryCount = (vizType: string): number =>
queryObjectCount?.[vizType] || 1;