blob: 0cb68d33c398b5f1baea4d5f74d7502bd68351d8 [file] [log] [blame]
/**
* 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>
);
};