mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
* fix(explore): only refresh data panel on relevant changes * add comment and supress warning
387 lines
10 KiB
TypeScript
387 lines
10 KiB
TypeScript
/**
|
|
* 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, useState } from 'react';
|
|
import { JsonObject, styled, t } from '@superset-ui/core';
|
|
import Collapse from 'src/components/Collapse';
|
|
import Tabs from 'src/components/Tabs';
|
|
import Loading from 'src/components/Loading';
|
|
import TableView, { EmptyWrapperType } from 'src/components/TableView';
|
|
import { getChartDataRequest } from 'src/chart/chartAction';
|
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
|
import {
|
|
getFromLocalStorage,
|
|
setInLocalStorage,
|
|
} from 'src/utils/localStorageHelpers';
|
|
import {
|
|
CopyToClipboardButton,
|
|
FilterInput,
|
|
RowCount,
|
|
useFilteredTableData,
|
|
useTableColumns,
|
|
} from 'src/explore/components/DataTableControl';
|
|
|
|
const RESULT_TYPES = {
|
|
results: 'results' as const,
|
|
samples: 'samples' as const,
|
|
};
|
|
|
|
const NULLISH_RESULTS_STATE = {
|
|
[RESULT_TYPES.results]: undefined,
|
|
[RESULT_TYPES.samples]: undefined,
|
|
};
|
|
|
|
const DATA_TABLE_PAGE_SIZE = 50;
|
|
|
|
const STORAGE_KEYS = {
|
|
isOpen: 'is_datapanel_open',
|
|
};
|
|
|
|
const DATAPANEL_KEY = 'data';
|
|
|
|
const TableControlsWrapper = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
span {
|
|
flex-shrink: 0;
|
|
}
|
|
`;
|
|
|
|
const SouthPane = styled.div`
|
|
position: relative;
|
|
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
|
z-index: 5;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const TabsWrapper = styled.div<{ contentHeight: number }>`
|
|
height: ${({ contentHeight }) => contentHeight}px;
|
|
overflow: hidden;
|
|
|
|
.table-condensed {
|
|
height: 100%;
|
|
overflow: auto;
|
|
}
|
|
`;
|
|
|
|
const CollapseWrapper = styled.div`
|
|
height: 100%;
|
|
|
|
.collapse-inner {
|
|
height: 100%;
|
|
|
|
.ant-collapse-item {
|
|
height: 100%;
|
|
|
|
.ant-collapse-content {
|
|
height: calc(100% - ${({ theme }) => theme.gridUnit * 8}px);
|
|
|
|
.ant-collapse-content-box {
|
|
padding-top: 0;
|
|
height: 100%;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const Error = styled.pre`
|
|
margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`};
|
|
`;
|
|
|
|
export const DataTablesPane = ({
|
|
queryFormData,
|
|
tableSectionHeight,
|
|
onCollapseChange,
|
|
chartStatus,
|
|
ownState,
|
|
errorMessage,
|
|
queriesResponse,
|
|
}: {
|
|
queryFormData: Record<string, any>;
|
|
tableSectionHeight: number;
|
|
chartStatus: string;
|
|
ownState?: JsonObject;
|
|
onCollapseChange: (openPanelName: string) => void;
|
|
errorMessage?: JSX.Element;
|
|
queriesResponse: Record<string, any>;
|
|
}) => {
|
|
const [data, setData] = useState<{
|
|
[RESULT_TYPES.results]?: Record<string, any>[];
|
|
[RESULT_TYPES.samples]?: Record<string, any>[];
|
|
}>(NULLISH_RESULTS_STATE);
|
|
const [isLoading, setIsLoading] = useState({
|
|
[RESULT_TYPES.results]: true,
|
|
[RESULT_TYPES.samples]: true,
|
|
});
|
|
const [columnNames, setColumnNames] = useState<string[]>([]);
|
|
const [error, setError] = useState(NULLISH_RESULTS_STATE);
|
|
const [filterText, setFilterText] = useState('');
|
|
const [activeTabKey, setActiveTabKey] = useState<string>(
|
|
RESULT_TYPES.results,
|
|
);
|
|
const [isRequestPending, setIsRequestPending] = useState<{
|
|
[RESULT_TYPES.results]?: boolean;
|
|
[RESULT_TYPES.samples]?: boolean;
|
|
}>(NULLISH_RESULTS_STATE);
|
|
const [panelOpen, setPanelOpen] = useState(
|
|
getFromLocalStorage(STORAGE_KEYS.isOpen, false),
|
|
);
|
|
|
|
const getData = useCallback(
|
|
(resultType: string) => {
|
|
setIsLoading(prevIsLoading => ({
|
|
...prevIsLoading,
|
|
[resultType]: true,
|
|
}));
|
|
return getChartDataRequest({
|
|
formData: queryFormData,
|
|
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,
|
|
}));
|
|
}
|
|
|
|
setIsLoading(prevIsLoading => ({
|
|
...prevIsLoading,
|
|
[resultType]: false,
|
|
}));
|
|
setError(prevError => ({
|
|
...prevError,
|
|
[resultType]: null,
|
|
}));
|
|
})
|
|
.catch(response => {
|
|
getClientErrorObject(response).then(({ error, message }) => {
|
|
setError(prevError => ({
|
|
...prevError,
|
|
[resultType]: error || message || t('Sorry, An error occurred'),
|
|
}));
|
|
setIsLoading(prevIsLoading => ({
|
|
...prevIsLoading,
|
|
[resultType]: false,
|
|
}));
|
|
});
|
|
});
|
|
},
|
|
[queryFormData],
|
|
);
|
|
|
|
useEffect(() => {
|
|
setInLocalStorage(STORAGE_KEYS.isOpen, panelOpen);
|
|
}, [panelOpen]);
|
|
|
|
useEffect(() => {
|
|
setIsRequestPending(prevState => ({
|
|
...prevState,
|
|
[RESULT_TYPES.results]: true,
|
|
}));
|
|
}, [queryFormData]);
|
|
|
|
useEffect(() => {
|
|
setIsRequestPending(prevState => ({
|
|
...prevState,
|
|
[RESULT_TYPES.samples]: true,
|
|
}));
|
|
}, [queryFormData?.adhoc_filters, queryFormData?.datasource]);
|
|
|
|
useEffect(() => {
|
|
if (queriesResponse && chartStatus === 'success') {
|
|
const { colnames } = queriesResponse[0];
|
|
setColumnNames([...colnames]);
|
|
}
|
|
}, [queriesResponse]);
|
|
|
|
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 filteredData = {
|
|
[RESULT_TYPES.results]: useFilteredTableData(
|
|
filterText,
|
|
data[RESULT_TYPES.results],
|
|
),
|
|
[RESULT_TYPES.samples]: useFilteredTableData(
|
|
filterText,
|
|
data[RESULT_TYPES.samples],
|
|
),
|
|
};
|
|
|
|
// 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 = {
|
|
[RESULT_TYPES.results]: useTableColumns(
|
|
columnNames,
|
|
data[RESULT_TYPES.results],
|
|
),
|
|
[RESULT_TYPES.samples]: useTableColumns(
|
|
columnNames,
|
|
data[RESULT_TYPES.samples],
|
|
),
|
|
};
|
|
|
|
const renderDataTable = (type: string) => {
|
|
if (isLoading[type]) {
|
|
return <Loading />;
|
|
}
|
|
if (error[type]) {
|
|
return <Error>{error[type]}</Error>;
|
|
}
|
|
if (data[type]) {
|
|
if (data[type]?.length === 0) {
|
|
return <span>No data</span>;
|
|
}
|
|
return (
|
|
<TableView
|
|
columns={columns[type]}
|
|
data={filteredData[type]}
|
|
pageSize={DATA_TABLE_PAGE_SIZE}
|
|
noDataText={t('No data')}
|
|
emptyWrapperType={EmptyWrapperType.Small}
|
|
className="table-condensed"
|
|
isPaginationSticky
|
|
showRowCount={false}
|
|
small
|
|
/>
|
|
);
|
|
}
|
|
if (errorMessage) {
|
|
return <Error>{errorMessage}</Error>;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const TableControls = (
|
|
<TableControlsWrapper>
|
|
<RowCount data={data[activeTabKey]} loading={isLoading[activeTabKey]} />
|
|
<CopyToClipboardButton data={data[activeTabKey]} columns={columnNames} />
|
|
<FilterInput onChangeHandler={setFilterText} />
|
|
</TableControlsWrapper>
|
|
);
|
|
|
|
const handleCollapseChange = (openPanelName: string) => {
|
|
onCollapseChange(openPanelName);
|
|
setPanelOpen(!!openPanelName);
|
|
};
|
|
|
|
return (
|
|
<SouthPane data-test="some-purposeful-instance">
|
|
<TabsWrapper contentHeight={tableSectionHeight}>
|
|
<CollapseWrapper>
|
|
<Collapse
|
|
accordion
|
|
bordered={false}
|
|
defaultActiveKey={panelOpen ? DATAPANEL_KEY : undefined}
|
|
onChange={handleCollapseChange}
|
|
bold
|
|
ghost
|
|
className="collapse-inner"
|
|
>
|
|
<Collapse.Panel header={t('Data')} key={DATAPANEL_KEY}>
|
|
<Tabs
|
|
fullWidth={false}
|
|
tabBarExtraContent={TableControls}
|
|
activeKey={activeTabKey}
|
|
onChange={setActiveTabKey}
|
|
>
|
|
<Tabs.TabPane
|
|
tab={t('View results')}
|
|
key={RESULT_TYPES.results}
|
|
>
|
|
{renderDataTable(RESULT_TYPES.results)}
|
|
</Tabs.TabPane>
|
|
<Tabs.TabPane
|
|
tab={t('View samples')}
|
|
key={RESULT_TYPES.samples}
|
|
>
|
|
{renderDataTable(RESULT_TYPES.samples)}
|
|
</Tabs.TabPane>
|
|
</Tabs>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
</CollapseWrapper>
|
|
</TabsWrapper>
|
|
</SouthPane>
|
|
);
|
|
};
|