mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
feat: multiple results pane on explore and dashboard (#20277)
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user