refactor: decouple DataTablesPane (#20109)
This commit is contained in:
parent
694f75d376
commit
e9007e3c2c
|
|
@ -481,7 +481,6 @@ export function exploreJSON(
|
|||
return Promise.all([
|
||||
chartDataRequestCaught,
|
||||
dispatch(triggerQuery(false, key)),
|
||||
dispatch(updateQueryFormData(formData, key)),
|
||||
...annotationLayers.map(annotation =>
|
||||
dispatch(
|
||||
runAnnotationQuery({
|
||||
|
|
@ -595,3 +594,17 @@ export function refreshChart(chartKey, force, dashboardId) {
|
|||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const getDatasetSamples = async (datasetId, force) => {
|
||||
const endpoint = `/api/v1/dataset/${datasetId}/samples?force=${force}`;
|
||||
try {
|
||||
const response = await SupersetClient.get({ endpoint });
|
||||
return response.json.result;
|
||||
} catch (err) {
|
||||
const clientError = await getClientErrorObject(err);
|
||||
throw new Error(
|
||||
clientError.message || clientError.error || t('Sorry, an error occurred'),
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ describe('chart actions', () => {
|
|||
const actionThunk = actions.postChartFormData({});
|
||||
|
||||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, success
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
// chart update, trigger query, success
|
||||
expect(dispatch.callCount).toBe(4);
|
||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
|
||||
});
|
||||
|
|
@ -116,43 +116,32 @@ describe('chart actions', () => {
|
|||
const actionThunk = actions.postChartFormData({});
|
||||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, success
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
expect(dispatch.callCount).toBe(4);
|
||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||
expect(dispatch.args[1][0].type).toBe(actions.TRIGGER_QUERY);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
|
||||
const actionThunk = actions.postChartFormData({});
|
||||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, success
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||
expect(dispatch.args[2][0].type).toBe(actions.UPDATE_QUERY_FORM_DATA);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch logEvent async action', () => {
|
||||
const actionThunk = actions.postChartFormData({});
|
||||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, success
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
// chart update, trigger query, success
|
||||
expect(dispatch.callCount).toBe(4);
|
||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||
expect(typeof dispatch.args[3][0]).toBe('function');
|
||||
|
||||
dispatch.args[3][0](dispatch);
|
||||
expect(dispatch.callCount).toBe(6);
|
||||
expect(dispatch.args[5][0].type).toBe(LOG_EVENT);
|
||||
dispatch.args[2][0](dispatch);
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
expect(dispatch.args[4][0].type).toBe(LOG_EVENT);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
|
||||
const actionThunk = actions.postChartFormData({});
|
||||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, success
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
// chart update, trigger query, success
|
||||
expect(dispatch.callCount).toBe(4);
|
||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||
expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED);
|
||||
expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -168,8 +157,8 @@ describe('chart actions', () => {
|
|||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, fail
|
||||
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_FAILED);
|
||||
expect(dispatch.callCount).toBe(4);
|
||||
expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_FAILED);
|
||||
setupDefaultFetchMock();
|
||||
});
|
||||
});
|
||||
|
|
@ -185,9 +174,9 @@ describe('chart actions', () => {
|
|||
const actionThunk = actions.postChartFormData({}, false, timeoutInSec);
|
||||
|
||||
return actionThunk(dispatch).then(() => {
|
||||
// chart update, trigger query, update form data, fail
|
||||
expect(dispatch.callCount).toBe(5);
|
||||
const updateFailedAction = dispatch.args[4][0];
|
||||
// chart update, trigger query, fail
|
||||
expect(dispatch.callCount).toBe(4);
|
||||
const updateFailedAction = dispatch.args[3][0];
|
||||
expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED);
|
||||
expect(updateFailedAction.queriesResponse[0].error).toBe('misc error');
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ describe('Dashboard', () => {
|
|||
removeSliceFromDashboard() {},
|
||||
triggerQuery() {},
|
||||
logEvent() {},
|
||||
updateQueryFormData() {},
|
||||
},
|
||||
initMessages: [],
|
||||
dashboardState,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { DatasourceType } from '@superset-ui/core';
|
||||
import { exploreActions } from 'src/explore/actions/exploreActions';
|
||||
import { DataTablesPane } from '.';
|
||||
|
||||
const createProps = () => ({
|
||||
|
|
@ -62,6 +64,16 @@ const createProps = () => ({
|
|||
colnames: [],
|
||||
},
|
||||
],
|
||||
datasource: {
|
||||
id: 0,
|
||||
name: '',
|
||||
type: DatasourceType.Table,
|
||||
columns: [],
|
||||
metrics: [],
|
||||
columnFormats: {},
|
||||
verboseMap: {},
|
||||
},
|
||||
actions: exploreActions,
|
||||
});
|
||||
|
||||
describe('DataTablesPane', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
/**
|
||||
* 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, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
MouseEvent,
|
||||
} from 'react';
|
||||
import { styled, t, useTheme } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import {
|
||||
getItem,
|
||||
setItem,
|
||||
LocalStorageKeys,
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import { ResultsPane, SamplesPane, TableControlsWrapper } from './components';
|
||||
import { DataTablesPaneProps } from './types';
|
||||
|
||||
enum ResultTypes {
|
||||
Results = 'results',
|
||||
Samples = 'samples',
|
||||
}
|
||||
|
||||
const SouthPane = styled.div`
|
||||
${({ theme }) => `
|
||||
position: relative;
|
||||
background-color: ${theme.colors.grayscale.light5};
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.table-condensed {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
margin-bottom: ${theme.gridUnit * 4}px;
|
||||
|
||||
.table {
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container > ul[role='navigation'] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const DataTablesPane = ({
|
||||
queryFormData,
|
||||
datasource,
|
||||
queryForce,
|
||||
onCollapseChange,
|
||||
ownState,
|
||||
errorMessage,
|
||||
actions,
|
||||
}: DataTablesPaneProps) => {
|
||||
const theme = useTheme();
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>(ResultTypes.Results);
|
||||
const [isRequest, setIsRequest] = useState<Record<ResultTypes, boolean>>({
|
||||
results: getItem(LocalStorageKeys.is_datapanel_open, false),
|
||||
samples: false,
|
||||
});
|
||||
const [panelOpen, setPanelOpen] = useState(
|
||||
getItem(LocalStorageKeys.is_datapanel_open, false),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setItem(LocalStorageKeys.is_datapanel_open, panelOpen);
|
||||
}, [panelOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!panelOpen) {
|
||||
setIsRequest({
|
||||
results: false,
|
||||
samples: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (panelOpen && activeTabKey === ResultTypes.Results) {
|
||||
setIsRequest({
|
||||
results: true,
|
||||
samples: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (panelOpen && activeTabKey === ResultTypes.Samples) {
|
||||
setIsRequest({
|
||||
results: false,
|
||||
samples: true,
|
||||
});
|
||||
}
|
||||
}, [panelOpen, activeTabKey]);
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
onCollapseChange(isOpen);
|
||||
setPanelOpen(isOpen);
|
||||
},
|
||||
[onCollapseChange],
|
||||
);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tabKey: string, e: MouseEvent) => {
|
||||
if (!panelOpen) {
|
||||
handleCollapseChange(true);
|
||||
} else if (tabKey === activeTabKey) {
|
||||
e.preventDefault();
|
||||
handleCollapseChange(false);
|
||||
}
|
||||
setActiveTabKey(tabKey);
|
||||
},
|
||||
[activeTabKey, handleCollapseChange, panelOpen],
|
||||
);
|
||||
|
||||
const CollapseButton = useMemo(() => {
|
||||
const caretIcon = panelOpen ? (
|
||||
<Icons.CaretUp
|
||||
iconColor={theme.colors.grayscale.base}
|
||||
aria-label={t('Collapse data panel')}
|
||||
/>
|
||||
) : (
|
||||
<Icons.CaretDown
|
||||
iconColor={theme.colors.grayscale.base}
|
||||
aria-label={t('Expand data panel')}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<TableControlsWrapper>
|
||||
{panelOpen ? (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleCollapseChange(false)}
|
||||
>
|
||||
{caretIcon}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleCollapseChange(true)}
|
||||
>
|
||||
{caretIcon}
|
||||
</span>
|
||||
)}
|
||||
</TableControlsWrapper>
|
||||
);
|
||||
}, [handleCollapseChange, panelOpen, theme.colors.grayscale.base]);
|
||||
|
||||
return (
|
||||
<SouthPane data-test="some-purposeful-instance">
|
||||
<Tabs
|
||||
fullWidth={false}
|
||||
tabBarExtraContent={CollapseButton}
|
||||
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>
|
||||
<Tabs.TabPane tab={t('Samples')} key={ResultTypes.Samples}>
|
||||
<SamplesPane
|
||||
datasource={datasource}
|
||||
queryForce={queryForce}
|
||||
isRequest={isRequest.samples}
|
||||
actions={actions}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</SouthPane>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* 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, { useMemo } from 'react';
|
||||
import { css, styled } from '@superset-ui/core';
|
||||
import {
|
||||
CopyToClipboardButton,
|
||||
FilterInput,
|
||||
RowCount,
|
||||
} from 'src/explore/components/DataTableControl';
|
||||
import { applyFormattingToTabularData } from 'src/utils/common';
|
||||
import { useOriginalFormattedTimeColumns } from 'src/explore/components/useOriginalFormattedTimeColumns';
|
||||
|
||||
export const TableControlsWrapper = styled.div`
|
||||
${({ theme }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
|
||||
span {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const TableControls = ({
|
||||
data,
|
||||
datasourceId,
|
||||
onInputChange,
|
||||
columnNames,
|
||||
isLoading,
|
||||
}: {
|
||||
data: Record<string, any>[];
|
||||
datasourceId?: string;
|
||||
onInputChange: (input: string) => void;
|
||||
columnNames: string[];
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const originalFormattedTimeColumns =
|
||||
useOriginalFormattedTimeColumns(datasourceId);
|
||||
const formattedData = useMemo(
|
||||
() => applyFormattingToTabularData(data, originalFormattedTimeColumns),
|
||||
[data, originalFormattedTimeColumns],
|
||||
);
|
||||
return (
|
||||
<TableControlsWrapper>
|
||||
<FilterInput onChangeHandler={onInputChange} />
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<RowCount data={data} loading={isLoading} />
|
||||
<CopyToClipboardButton data={formattedData} columns={columnNames} />
|
||||
</div>
|
||||
</TableControlsWrapper>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* 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, useEffect } from 'react';
|
||||
import { ensureIsArray, GenericDataType, 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 { useOriginalFormattedTimeColumns } from 'src/explore/components/useOriginalFormattedTimeColumns';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { TableControls } from './DataTableControls';
|
||||
import { ResultsPaneProps } from '../types';
|
||||
|
||||
const Error = styled.pre`
|
||||
margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`};
|
||||
`;
|
||||
|
||||
const cache = new WeakSet();
|
||||
|
||||
export const ResultsPane = ({
|
||||
isRequest,
|
||||
queryFormData,
|
||||
queryForce,
|
||||
ownState,
|
||||
errorMessage,
|
||||
actions,
|
||||
dataSize = 50,
|
||||
}: ResultsPaneProps) => {
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [data, setData] = useState<Record<string, any>[][]>([]);
|
||||
const [colnames, setColnames] = useState<string[]>([]);
|
||||
const [coltypes, setColtypes] = useState<GenericDataType[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [responseError, setResponseError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
// it's an invalid formData when gets a errorMessage
|
||||
if (errorMessage) return;
|
||||
if (isRequest && !cache.has(queryFormData)) {
|
||||
setIsLoading(true);
|
||||
getChartDataRequest({
|
||||
formData: queryFormData,
|
||||
force: queryForce,
|
||||
resultFormat: 'json',
|
||||
resultType: 'results',
|
||||
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);
|
||||
}
|
||||
setResponseError('');
|
||||
cache.add(queryFormData);
|
||||
if (queryForce && actions) {
|
||||
actions.setForceQuery(false);
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(({ error, message }) => {
|
||||
setResponseError(error || message || t('Sorry, an error occurred'));
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [queryFormData, isRequest]);
|
||||
|
||||
const originalFormattedTimeColumns = useOriginalFormattedTimeColumns(
|
||||
queryFormData.datasource,
|
||||
);
|
||||
// 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,
|
||||
originalFormattedTimeColumns,
|
||||
);
|
||||
const filteredData = useFilteredTableData(filterText, data);
|
||||
|
||||
if (errorMessage) {
|
||||
const title = t('Run a query to display results');
|
||||
return <EmptyStateMedium image="document.svg" title={title} />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (responseError) {
|
||||
return (
|
||||
<>
|
||||
<TableControls
|
||||
data={filteredData}
|
||||
columnNames={colnames}
|
||||
datasourceId={queryFormData?.datasource}
|
||||
onInputChange={input => setFilterText(input)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Error>{responseError}</Error>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
const title = t('No results were returned for this query');
|
||||
return <EmptyStateMedium image="document.svg" title={title} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableControls
|
||||
data={filteredData}
|
||||
columnNames={colnames}
|
||||
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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* 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, useEffect, useMemo } from 'react';
|
||||
import { GenericDataType, 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 { useOriginalFormattedTimeColumns } from 'src/explore/components/useOriginalFormattedTimeColumns';
|
||||
import { getDatasetSamples } from 'src/components/Chart/chartAction';
|
||||
import { TableControls } from './DataTableControls';
|
||||
import { SamplesPaneProps } from '../types';
|
||||
|
||||
const Error = styled.pre`
|
||||
margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`};
|
||||
`;
|
||||
|
||||
const cache = new WeakSet();
|
||||
|
||||
export const SamplesPane = ({
|
||||
isRequest,
|
||||
datasource,
|
||||
queryForce,
|
||||
actions,
|
||||
dataSize = 50,
|
||||
}: SamplesPaneProps) => {
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [data, setData] = useState<Record<string, any>[][]>([]);
|
||||
const [colnames, setColnames] = useState<string[]>([]);
|
||||
const [coltypes, setColtypes] = useState<GenericDataType[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [responseError, setResponseError] = useState<string>('');
|
||||
const datasourceId = useMemo(
|
||||
() => `${datasource.id}__${datasource.type}`,
|
||||
[datasource],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRequest && queryForce) {
|
||||
cache.delete(datasource);
|
||||
}
|
||||
|
||||
if (isRequest && !cache.has(datasource)) {
|
||||
setIsLoading(true);
|
||||
getDatasetSamples(datasource.id, queryForce)
|
||||
.then(response => {
|
||||
setData(response.data);
|
||||
setColnames(response.colnames);
|
||||
setColtypes(response.coltypes);
|
||||
setResponseError('');
|
||||
cache.add(datasource);
|
||||
if (queryForce && actions) {
|
||||
actions.setForceQuery(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
setResponseError(`${error.name}: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [datasource, isRequest, queryForce]);
|
||||
|
||||
const originalFormattedTimeColumns =
|
||||
useOriginalFormattedTimeColumns(datasourceId);
|
||||
// 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,
|
||||
originalFormattedTimeColumns,
|
||||
);
|
||||
const filteredData = useFilteredTableData(filterText, data);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (responseError) {
|
||||
return (
|
||||
<>
|
||||
<TableControls
|
||||
data={filteredData}
|
||||
columnNames={colnames}
|
||||
datasourceId={datasourceId}
|
||||
onInputChange={input => setFilterText(input)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Error>{responseError}</Error>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
const title = t('No samples were returned for this dataset');
|
||||
return <EmptyStateMedium image="document.svg" title={title} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableControls
|
||||
data={filteredData}
|
||||
columnNames={colnames}
|
||||
datasourceId={datasourceId}
|
||||
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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export { ResultsPane } from './ResultsPane';
|
||||
export { SamplesPane } from './SamplesPane';
|
||||
export { TableControls, TableControlsWrapper } from './DataTableControls';
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
export { DataTablesPane } from './DataTablesPane';
|
||||
export * from './components';
|
||||
|
|
@ -1,532 +0,0 @@
|
|||
/**
|
||||
* 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, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
MouseEvent,
|
||||
} from 'react';
|
||||
import {
|
||||
css,
|
||||
ensureIsArray,
|
||||
GenericDataType,
|
||||
JsonObject,
|
||||
styled,
|
||||
t,
|
||||
useTheme,
|
||||
} from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import Loading from 'src/components/Loading';
|
||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||
import TableView, { EmptyWrapperType } from 'src/components/TableView';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import {
|
||||
getItem,
|
||||
setItem,
|
||||
LocalStorageKeys,
|
||||
} from 'src/utils/localStorageHelpers';
|
||||
import {
|
||||
CopyToClipboardButton,
|
||||
FilterInput,
|
||||
RowCount,
|
||||
useFilteredTableData,
|
||||
useTableColumns,
|
||||
} from 'src/explore/components/DataTableControl';
|
||||
import { applyFormattingToTabularData } from 'src/utils/common';
|
||||
import { useOriginalFormattedTimeColumns } from '../useOriginalFormattedTimeColumns';
|
||||
|
||||
const RESULT_TYPES = {
|
||||
results: 'results' as const,
|
||||
samples: 'samples' as const,
|
||||
};
|
||||
|
||||
const getDefaultDataTablesState = (value: any) => ({
|
||||
[RESULT_TYPES.results]: value,
|
||||
[RESULT_TYPES.samples]: value,
|
||||
});
|
||||
|
||||
const DATA_TABLE_PAGE_SIZE = 50;
|
||||
|
||||
const TableControlsWrapper = styled.div`
|
||||
${({ theme }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
|
||||
span {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const SouthPane = styled.div`
|
||||
${({ theme }) => `
|
||||
position: relative;
|
||||
background-color: ${theme.colors.grayscale.light5};
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.table-condensed {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
margin-bottom: ${theme.gridUnit * 4}px;
|
||||
|
||||
.table {
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container > ul[role='navigation'] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Error = styled.pre`
|
||||
margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`};
|
||||
`;
|
||||
|
||||
interface DataTableProps {
|
||||
columnNames: string[];
|
||||
columnTypes: GenericDataType[] | undefined;
|
||||
datasource: string | undefined;
|
||||
filterText: string;
|
||||
data: object[] | undefined;
|
||||
isLoading: boolean;
|
||||
error: string | undefined;
|
||||
errorMessage: React.ReactElement | undefined;
|
||||
type: 'results' | 'samples';
|
||||
}
|
||||
|
||||
const DataTable = ({
|
||||
columnNames,
|
||||
columnTypes,
|
||||
datasource,
|
||||
filterText,
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
errorMessage,
|
||||
type,
|
||||
}: DataTableProps) => {
|
||||
const originalFormattedTimeColumns =
|
||||
useOriginalFormattedTimeColumns(datasource);
|
||||
// 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,
|
||||
columnTypes,
|
||||
data,
|
||||
datasource,
|
||||
originalFormattedTimeColumns,
|
||||
);
|
||||
const filteredData = useFilteredTableData(filterText, data);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (error) {
|
||||
return <Error>{error}</Error>;
|
||||
}
|
||||
if (data) {
|
||||
if (data.length === 0) {
|
||||
const title =
|
||||
type === 'samples'
|
||||
? t('No samples were returned for this query')
|
||||
: t('No results were returned for this query');
|
||||
return <EmptyStateMedium image="document.svg" title={title} />;
|
||||
}
|
||||
return (
|
||||
<TableView
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
pageSize={DATA_TABLE_PAGE_SIZE}
|
||||
noDataText={t('No results')}
|
||||
emptyWrapperType={EmptyWrapperType.Small}
|
||||
className="table-condensed"
|
||||
isPaginationSticky
|
||||
showRowCount={false}
|
||||
small
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (errorMessage) {
|
||||
const title =
|
||||
type === 'samples'
|
||||
? t('Run a query to display samples')
|
||||
: t('Run a query to display results');
|
||||
return <EmptyStateMedium image="document.svg" title={title} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const TableControls = ({
|
||||
data,
|
||||
datasourceId,
|
||||
onInputChange,
|
||||
columnNames,
|
||||
isLoading,
|
||||
}: {
|
||||
data: Record<string, any>[];
|
||||
datasourceId?: string;
|
||||
onInputChange: (input: string) => void;
|
||||
columnNames: string[];
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const originalFormattedTimeColumns =
|
||||
useOriginalFormattedTimeColumns(datasourceId);
|
||||
const formattedData = useMemo(
|
||||
() => applyFormattingToTabularData(data, originalFormattedTimeColumns),
|
||||
[data, originalFormattedTimeColumns],
|
||||
);
|
||||
return (
|
||||
<TableControlsWrapper>
|
||||
<FilterInput onChangeHandler={onInputChange} />
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<RowCount data={data} loading={isLoading} />
|
||||
<CopyToClipboardButton data={formattedData} columns={columnNames} />
|
||||
</div>
|
||||
</TableControlsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataTablesPane = ({
|
||||
queryFormData,
|
||||
queryForce,
|
||||
onCollapseChange,
|
||||
chartStatus,
|
||||
ownState,
|
||||
errorMessage,
|
||||
queriesResponse,
|
||||
}: {
|
||||
queryFormData: Record<string, any>;
|
||||
queryForce: boolean;
|
||||
chartStatus: string;
|
||||
ownState?: JsonObject;
|
||||
onCollapseChange: (isOpen: boolean) => void;
|
||||
errorMessage?: JSX.Element;
|
||||
queriesResponse: Record<string, any>;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
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(getDefaultDataTablesState(''));
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>(
|
||||
RESULT_TYPES.results,
|
||||
);
|
||||
const [isRequestPending, setIsRequestPending] = useState(
|
||||
getDefaultDataTablesState(false),
|
||||
);
|
||||
const [panelOpen, setPanelOpen] = useState(
|
||||
getItem(LocalStorageKeys.is_datapanel_open, false),
|
||||
);
|
||||
|
||||
const getData = useCallback(
|
||||
(resultType: 'samples' | 'results') => {
|
||||
setIsLoading(prevIsLoading => ({
|
||||
...prevIsLoading,
|
||||
[resultType]: true,
|
||||
}));
|
||||
return getChartDataRequest({
|
||||
formData: queryFormData,
|
||||
force: queryForce,
|
||||
resultFormat: 'json',
|
||||
resultType,
|
||||
ownState,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
// Only displaying the first query is currently supported
|
||||
if (json.result.length > 1) {
|
||||
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(prevData => ({
|
||||
...prevData,
|
||||
[resultType]: data,
|
||||
}));
|
||||
} else {
|
||||
setData(prevData => ({
|
||||
...prevData,
|
||||
[resultType]: json.result[0].data,
|
||||
}));
|
||||
}
|
||||
|
||||
const colNames = ensureIsArray(json.result[0].colnames);
|
||||
|
||||
setColumnNames(prevColumnNames => ({
|
||||
...prevColumnNames,
|
||||
[resultType]: colNames,
|
||||
}));
|
||||
setColumnTypes(prevColumnTypes => ({
|
||||
...prevColumnTypes,
|
||||
[resultType]: json.result[0].coltypes || [],
|
||||
}));
|
||||
setIsLoading(prevIsLoading => ({
|
||||
...prevIsLoading,
|
||||
[resultType]: false,
|
||||
}));
|
||||
setError(prevError => ({
|
||||
...prevError,
|
||||
[resultType]: undefined,
|
||||
}));
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(({ error, message }) => {
|
||||
setError(prevError => ({
|
||||
...prevError,
|
||||
[resultType]: error || message || t('Sorry, an error occurred'),
|
||||
}));
|
||||
setIsLoading(prevIsLoading => ({
|
||||
...prevIsLoading,
|
||||
[resultType]: false,
|
||||
}));
|
||||
});
|
||||
});
|
||||
},
|
||||
[queryFormData, columnNames],
|
||||
);
|
||||
useEffect(() => {
|
||||
setItem(LocalStorageKeys.is_datapanel_open, panelOpen);
|
||||
}, [panelOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRequestPending(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.results]: true,
|
||||
}));
|
||||
}, [queryFormData]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRequestPending(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.samples]: true,
|
||||
}));
|
||||
}, [queryFormData?.datasource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queriesResponse && chartStatus === 'success') {
|
||||
const { colnames } = queriesResponse[0];
|
||||
setColumnNames(prevColumnNames => ({
|
||||
...prevColumnNames,
|
||||
[RESULT_TYPES.results]: colnames ?? [],
|
||||
}));
|
||||
}
|
||||
}, [queriesResponse, chartStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelOpen && isRequestPending[RESULT_TYPES.results]) {
|
||||
if (errorMessage) {
|
||||
setIsRequestPending(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.results]: false,
|
||||
}));
|
||||
setIsLoading(prevIsLoading => ({
|
||||
...prevIsLoading,
|
||||
[RESULT_TYPES.results]: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (chartStatus === 'loading') {
|
||||
setIsLoading(prevIsLoading => ({
|
||||
...prevIsLoading,
|
||||
[RESULT_TYPES.results]: true,
|
||||
}));
|
||||
} else {
|
||||
setIsRequestPending(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.results]: false,
|
||||
}));
|
||||
getData(RESULT_TYPES.results);
|
||||
}
|
||||
}
|
||||
if (
|
||||
panelOpen &&
|
||||
isRequestPending[RESULT_TYPES.samples] &&
|
||||
activeTabKey === RESULT_TYPES.samples
|
||||
) {
|
||||
setIsRequestPending(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.samples]: false,
|
||||
}));
|
||||
getData(RESULT_TYPES.samples);
|
||||
}
|
||||
}, [
|
||||
panelOpen,
|
||||
isRequestPending,
|
||||
getData,
|
||||
activeTabKey,
|
||||
chartStatus,
|
||||
errorMessage,
|
||||
]);
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
onCollapseChange(isOpen);
|
||||
setPanelOpen(isOpen);
|
||||
},
|
||||
[onCollapseChange],
|
||||
);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tabKey: string, e: MouseEvent) => {
|
||||
if (!panelOpen) {
|
||||
handleCollapseChange(true);
|
||||
} else if (tabKey === activeTabKey) {
|
||||
e.preventDefault();
|
||||
handleCollapseChange(false);
|
||||
}
|
||||
setActiveTabKey(tabKey);
|
||||
},
|
||||
[activeTabKey, handleCollapseChange, panelOpen],
|
||||
);
|
||||
|
||||
const CollapseButton = useMemo(() => {
|
||||
const caretIcon = panelOpen ? (
|
||||
<Icons.CaretUp
|
||||
iconColor={theme.colors.grayscale.base}
|
||||
aria-label={t('Collapse data panel')}
|
||||
/>
|
||||
) : (
|
||||
<Icons.CaretDown
|
||||
iconColor={theme.colors.grayscale.base}
|
||||
aria-label={t('Expand data panel')}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<TableControlsWrapper>
|
||||
{panelOpen ? (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleCollapseChange(false)}
|
||||
>
|
||||
{caretIcon}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleCollapseChange(true)}
|
||||
>
|
||||
{caretIcon}
|
||||
</span>
|
||||
)}
|
||||
</TableControlsWrapper>
|
||||
);
|
||||
}, [handleCollapseChange, panelOpen, theme.colors.grayscale.base]);
|
||||
|
||||
return (
|
||||
<SouthPane data-test="some-purposeful-instance">
|
||||
<Tabs
|
||||
fullWidth={false}
|
||||
tabBarExtraContent={CollapseButton}
|
||||
activeKey={panelOpen ? activeTabKey : ''}
|
||||
onTabClick={handleTabClick}
|
||||
>
|
||||
<Tabs.TabPane tab={t('Results')} key={RESULT_TYPES.results}>
|
||||
<TableControls
|
||||
data={data[RESULT_TYPES.results]}
|
||||
columnNames={columnNames[RESULT_TYPES.results]}
|
||||
datasourceId={queryFormData?.datasource}
|
||||
onInputChange={input =>
|
||||
setFilterText(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.results]: input,
|
||||
}))
|
||||
}
|
||||
isLoading={isLoading[RESULT_TYPES.results]}
|
||||
/>
|
||||
<DataTable
|
||||
isLoading={isLoading[RESULT_TYPES.results]}
|
||||
data={data[RESULT_TYPES.results]}
|
||||
datasource={queryFormData?.datasource}
|
||||
columnNames={columnNames[RESULT_TYPES.results]}
|
||||
columnTypes={columnTypes[RESULT_TYPES.results]}
|
||||
filterText={filterText[RESULT_TYPES.results]}
|
||||
error={error[RESULT_TYPES.results]}
|
||||
errorMessage={errorMessage}
|
||||
type={RESULT_TYPES.results}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('Samples')} key={RESULT_TYPES.samples}>
|
||||
<TableControls
|
||||
data={data[RESULT_TYPES.samples]}
|
||||
columnNames={columnNames[RESULT_TYPES.samples]}
|
||||
datasourceId={queryFormData?.datasource}
|
||||
onInputChange={input =>
|
||||
setFilterText(prevState => ({
|
||||
...prevState,
|
||||
[RESULT_TYPES.samples]: input,
|
||||
}))
|
||||
}
|
||||
isLoading={isLoading[RESULT_TYPES.samples]}
|
||||
/>
|
||||
<DataTable
|
||||
isLoading={isLoading[RESULT_TYPES.samples]}
|
||||
data={data[RESULT_TYPES.samples]}
|
||||
datasource={queryFormData?.datasource}
|
||||
columnNames={columnNames[RESULT_TYPES.samples]}
|
||||
columnTypes={columnTypes[RESULT_TYPES.samples]}
|
||||
filterText={filterText[RESULT_TYPES.samples]}
|
||||
error={error[RESULT_TYPES.samples]}
|
||||
errorMessage={errorMessage}
|
||||
type={RESULT_TYPES.samples}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</SouthPane>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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 { Datasource, JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||
|
||||
export interface DataTablesPaneProps {
|
||||
queryFormData: QueryFormData;
|
||||
datasource: Datasource;
|
||||
queryForce: boolean;
|
||||
ownState?: JsonObject;
|
||||
onCollapseChange: (isOpen: boolean) => void;
|
||||
errorMessage?: JSX.Element;
|
||||
actions: ExploreActions;
|
||||
}
|
||||
|
||||
export interface ResultsPaneProps {
|
||||
isRequest: boolean;
|
||||
queryFormData: QueryFormData;
|
||||
queryForce: boolean;
|
||||
ownState?: JsonObject;
|
||||
errorMessage?: React.ReactElement;
|
||||
actions?: ExploreActions;
|
||||
dataSize?: number;
|
||||
}
|
||||
|
||||
export interface SamplesPaneProps {
|
||||
isRequest: boolean;
|
||||
datasource: Datasource;
|
||||
queryForce: boolean;
|
||||
actions?: ExploreActions;
|
||||
dataSize?: number;
|
||||
}
|
||||
|
|
@ -97,6 +97,7 @@ const createProps = () => ({
|
|||
fetchFaveStar: jest.fn(),
|
||||
saveFaveStar: jest.fn(),
|
||||
redirectSQLLab: jest.fn(),
|
||||
updateQueryFormData: jest.fn(),
|
||||
},
|
||||
user: {
|
||||
userId: 1,
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ const ExploreChartPanel = ({
|
|||
undefined,
|
||||
ownState,
|
||||
);
|
||||
actions.updateQueryFormData(formData, chart.id);
|
||||
}, [actions, chart.id, formData, ownState, timeout]);
|
||||
|
||||
const onCollapseChange = useCallback(isOpen => {
|
||||
|
|
@ -388,11 +389,11 @@ const ExploreChartPanel = ({
|
|||
<DataTablesPane
|
||||
ownState={ownState}
|
||||
queryFormData={queryFormData}
|
||||
datasource={datasource}
|
||||
queryForce={force}
|
||||
onCollapseChange={onCollapseChange}
|
||||
chartStatus={chart.chartStatus}
|
||||
errorMessage={errorMessage}
|
||||
queriesResponse={chart.queriesResponse}
|
||||
actions={actions}
|
||||
/>
|
||||
</Split>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import ChartContainer from 'src/explore/components/ExploreChartPanel';
|
||||
|
||||
const createProps = (overrides = {}) => ({
|
||||
sliceName: 'Trend Line',
|
||||
vizType: 'line',
|
||||
height: '500px',
|
||||
actions: {},
|
||||
can_overwrite: false,
|
||||
|
|
@ -30,9 +30,29 @@ const createProps = (overrides = {}) => ({
|
|||
containerId: 'foo',
|
||||
width: '500px',
|
||||
isStarred: false,
|
||||
chartIsStale: false,
|
||||
chart: {},
|
||||
form_data: {},
|
||||
vizType: 'histogram',
|
||||
chart: {
|
||||
id: 1,
|
||||
latestQueryFormData: {
|
||||
viz_type: 'histogram',
|
||||
datasource: '49__table',
|
||||
slice_id: 318,
|
||||
url_params: {},
|
||||
granularity_sqla: 'time_start',
|
||||
time_range: 'No filter',
|
||||
all_columns_x: ['age'],
|
||||
adhoc_filters: [],
|
||||
row_limit: 10000,
|
||||
groupby: null,
|
||||
color_scheme: 'supersetColors',
|
||||
label_colors: {},
|
||||
link_length: '25',
|
||||
x_axis_label: 'age',
|
||||
y_axis_label: 'count',
|
||||
},
|
||||
chartStatus: 'rendered',
|
||||
queriesResponse: [{ is_cached: true }],
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
@ -83,4 +103,37 @@ describe('ChartContainer', () => {
|
|||
screen.getByText('Required control values have been removed'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render cached button and call expected actions', () => {
|
||||
const setForceQuery = jest.fn();
|
||||
const postChartFormData = jest.fn();
|
||||
const updateQueryFormData = jest.fn();
|
||||
const props = createProps({
|
||||
actions: {
|
||||
setForceQuery,
|
||||
postChartFormData,
|
||||
updateQueryFormData,
|
||||
},
|
||||
});
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
|
||||
const cached = screen.queryByText('Cached');
|
||||
expect(cached).toBeInTheDocument();
|
||||
|
||||
userEvent.click(cached);
|
||||
expect(setForceQuery).toHaveBeenCalledTimes(1);
|
||||
expect(postChartFormData).toHaveBeenCalledTimes(1);
|
||||
expect(updateQueryFormData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should hide cached button', () => {
|
||||
const props = createProps({
|
||||
chart: {
|
||||
chartStatus: 'rendered',
|
||||
queriesResponse: [{ is_cached: false }],
|
||||
},
|
||||
});
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
expect(screen.queryByText('Cached')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ function ExploreViewContainer(props) {
|
|||
const onQuery = useCallback(() => {
|
||||
props.actions.setForceQuery(false);
|
||||
props.actions.triggerQuery(true, props.chart.id);
|
||||
props.actions.updateQueryFormData(props.form_data, props.chart.id);
|
||||
addHistory();
|
||||
setLastQueriedControls(props.controls);
|
||||
}, [props.controls, addHistory, props.actions, props.chart.id]);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ import { ExplorePageState } from '../reducers/getInitialState';
|
|||
export const useOriginalFormattedTimeColumns = (datasourceId?: string) =>
|
||||
useSelector<ExplorePageState, string[]>(state =>
|
||||
datasourceId
|
||||
? state.explore.originalFormattedTimeColumns?.[datasourceId] ?? []
|
||||
? state?.explore?.originalFormattedTimeColumns?.[datasourceId] ?? []
|
||||
: [],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ from io import BytesIO
|
|||
from typing import Any
|
||||
from zipfile import is_zipfile, ZipFile
|
||||
|
||||
import simplejson
|
||||
import yaml
|
||||
from flask import g, request, Response, send_file
|
||||
from flask import g, make_response, request, Response, send_file
|
||||
from flask_appbuilder.api import expose, protect, rison, safe
|
||||
from flask_appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask_babel import ngettext
|
||||
|
|
@ -62,7 +63,7 @@ from superset.datasets.schemas import (
|
|||
get_delete_ids_schema,
|
||||
get_export_ids_schema,
|
||||
)
|
||||
from superset.utils.core import parse_boolean_string
|
||||
from superset.utils.core import json_int_dttm_ser, parse_boolean_string
|
||||
from superset.views.base import DatasourceFilter, generate_download_headers
|
||||
from superset.views.base_api import (
|
||||
BaseSupersetModelRestApi,
|
||||
|
|
@ -811,7 +812,14 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
try:
|
||||
force = parse_boolean_string(request.args.get("force"))
|
||||
rv = SamplesDatasetCommand(g.user, pk, force).run()
|
||||
return self.response(200, result=rv)
|
||||
response_data = simplejson.dumps(
|
||||
{"result": rv},
|
||||
default=json_int_dttm_ser,
|
||||
ignore_nan=True,
|
||||
)
|
||||
resp = make_response(response_data, 200)
|
||||
resp.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return resp
|
||||
except DatasetNotFoundError:
|
||||
return self.response_404()
|
||||
except DatasetForbiddenError:
|
||||
|
|
|
|||
Loading…
Reference in New Issue