feat: Results table on Explore view (#11854)
* Upgrade react-split * Implement split on ExploreChartPanel * Implement tabs with collapse * Fix run query button * Fix copy to clipboard button * Move table controls to separate file * Make component more generic to handle samples * Remove samples from DisplayQueryButton * Move data tables to separate file * Make split dynamically controlled * Fix unit test * Fix arrow not centered * fixup! Fix arrow not centered * Change copy button paddings * Use translations * Fix grammar in a comment * Fix imports * Use grid units * Convert new files to typescript * Fix after rebase * Remove forceRender * Fix big_number test * Delay making request until panel is opened * White background in south panel * fixup! White background in south panel * Lint fix * Lint fix * Remove redundant prop types * Remove console log * Delay loading samples until user switches tab * Add debounce to filter input * Use gridUnit for gutter sizes * Change types object to Record<string, any>
This commit is contained in:
parent
66cd565bff
commit
41738df77d
|
|
@ -75,8 +75,8 @@ describe('Visualization > Big Number with Trendline', () => {
|
|||
...BIG_NUMBER_FORM_DATA,
|
||||
show_trend_line: false,
|
||||
});
|
||||
cy.get('.chart-container .header-line');
|
||||
cy.get('.chart-container .subheader-line');
|
||||
cy.get('.chart-container svg').should('not.exist');
|
||||
cy.get('[data-test="chart-container"] .header-line');
|
||||
cy.get('[data-test="chart-container"] .subheader-line');
|
||||
cy.get('[data-test="chart-container"] svg').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43761,12 +43761,12 @@
|
|||
}
|
||||
},
|
||||
"react-split": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.4.tgz",
|
||||
"integrity": "sha512-NBKm9MaqzG/00laMUaS8GS9RnItVSekNNwItgGLMbFTeUa9w4bIY8Co/LszNBnpza9n2am0MXIw3SmyiMnhs+w==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.9.tgz",
|
||||
"integrity": "sha512-IxKtxxmcbNUmWMSd5vlNnlE0jwbgQS1HyQYxt7h8qFgPskSkUTNzMbO838xapmmNf9D+u9B/bdtFnVjt+JC2JA==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.7",
|
||||
"split.js": "^1.5.9"
|
||||
"split.js": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"react-split-pane": {
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
"react-select": "^3.1.0",
|
||||
"react-select-async-paginate": "^0.4.1",
|
||||
"react-sortable-hoc": "^1.11.0",
|
||||
"react-split": "^2.0.4",
|
||||
"react-split": "^2.0.9",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-syntax-highlighter": "^15.3.0",
|
||||
"react-table": "^7.2.1",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ describe('DisplayQueryButton', () => {
|
|||
theme: supersetTheme,
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(ModalTrigger)).toHaveLength(3);
|
||||
expect(wrapper.find(Menu.Item)).toHaveLength(5);
|
||||
expect(wrapper.find(ModalTrigger)).toHaveLength(1);
|
||||
expect(wrapper.find(Menu.Item)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
const Styles = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
.chart-tooltip {
|
||||
opacity: 0.75;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* 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 { styled, t } from '@superset-ui/core';
|
||||
import { FormControl } from 'react-bootstrap';
|
||||
import debounce from 'lodash/debounce';
|
||||
import Button from 'src/components/Button';
|
||||
import {
|
||||
applyFormattingToTabularData,
|
||||
prepareCopyToClipboardTabularData,
|
||||
} from 'src/utils/common';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import RowCountLabel from './RowCountLabel';
|
||||
|
||||
export const CopyButton = styled(Button)`
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
|
||||
// needed to override button's first-of-type margin: 0
|
||||
&& {
|
||||
margin: 0 ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
i {
|
||||
padding: 0 ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CopyToClipboardButton = ({
|
||||
data,
|
||||
}: {
|
||||
data?: Record<string, any>;
|
||||
}) => (
|
||||
<CopyToClipboard
|
||||
text={data ? prepareCopyToClipboardTabularData(data) : ''}
|
||||
wrapped={false}
|
||||
copyNode={
|
||||
<CopyButton buttonSize="xs">
|
||||
<i className="fa fa-clipboard" />
|
||||
</CopyButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const FilterInput = ({
|
||||
onChangeHandler,
|
||||
}: {
|
||||
onChangeHandler(filterText: string): void;
|
||||
}) => {
|
||||
const debouncedChangeHandler = debounce(onChangeHandler, 500);
|
||||
return (
|
||||
<FormControl
|
||||
placeholder={t('Search')}
|
||||
bsSize="sm"
|
||||
onChange={(event: any) => {
|
||||
const filterText = event.target.value;
|
||||
debouncedChangeHandler(filterText);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RowCount = ({ data }: { data?: Record<string, any>[] }) => (
|
||||
<RowCountLabel rowcount={data?.length ?? 0} suffix={t('rows retrieved')} />
|
||||
);
|
||||
|
||||
export const useFilteredTableData = (
|
||||
filterText: string,
|
||||
data?: Record<string, any>[],
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (!data?.length) {
|
||||
return [];
|
||||
}
|
||||
const formattedData = applyFormattingToTabularData(data);
|
||||
return formattedData.filter((row: Record<string, any>) =>
|
||||
Object.values(row).some(value =>
|
||||
value.toString().toLowerCase().includes(filterText.toLowerCase()),
|
||||
),
|
||||
);
|
||||
}, [data, filterText]);
|
||||
|
||||
export const useTableColumns = (data?: Record<string, any>[]) =>
|
||||
useMemo(
|
||||
() =>
|
||||
data?.length
|
||||
? Object.keys(data[0]).map(key => ({ accessor: key, Header: key }))
|
||||
: [],
|
||||
[data],
|
||||
);
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
/**
|
||||
* 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 { styled, t } from '@superset-ui/core';
|
||||
import { Collapse } from 'src/common/components';
|
||||
import Tabs from 'src/common/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 {
|
||||
CopyToClipboardButton,
|
||||
FilterInput,
|
||||
RowCount,
|
||||
useFilteredTableData,
|
||||
useTableColumns,
|
||||
} from './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 TableControlsWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const SouthPane = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const SouthPaneBackground = styled.div`
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
`;
|
||||
|
||||
const TabsWrapper = styled.div<{ contentHeight: number }>`
|
||||
height: ${({ contentHeight }) => contentHeight}px;
|
||||
overflow: hidden;
|
||||
|
||||
.table-condensed {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DataTablesPane = ({
|
||||
queryFormData,
|
||||
tableSectionHeight,
|
||||
onCollapseChange,
|
||||
displayBackground,
|
||||
}: {
|
||||
queryFormData: Record<string, any>;
|
||||
tableSectionHeight: number;
|
||||
onCollapseChange: (openPanelName: string) => void;
|
||||
displayBackground: boolean;
|
||||
}) => {
|
||||
const [data, setData] = useState<{
|
||||
[RESULT_TYPES.results]?: Record<string, any>[];
|
||||
[RESULT_TYPES.samples]?: Record<string, any>[];
|
||||
}>(NULLISH_RESULTS_STATE);
|
||||
const [isLoading, setIsLoading] = useState(NULLISH_RESULTS_STATE);
|
||||
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(false);
|
||||
|
||||
const getData = useCallback(
|
||||
(resultType: string) => {
|
||||
setIsLoading(prevIsLoading => ({ ...prevIsLoading, [resultType]: true }));
|
||||
return getChartDataRequest({
|
||||
formData: queryFormData,
|
||||
resultFormat: 'json',
|
||||
resultType,
|
||||
})
|
||||
.then(response => {
|
||||
// Only displaying the first query is currently supported
|
||||
const result = response.result[0];
|
||||
setData(prevData => ({ ...prevData, [resultType]: result.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(() => {
|
||||
setIsRequestPending({
|
||||
[RESULT_TYPES.results]: true,
|
||||
[RESULT_TYPES.samples]: true,
|
||||
});
|
||||
}, [queryFormData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelOpen && isRequestPending[RESULT_TYPES.results]) {
|
||||
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]);
|
||||
|
||||
const filteredData = {
|
||||
[RESULT_TYPES.results]: useFilteredTableData(
|
||||
filterText,
|
||||
data[RESULT_TYPES.results],
|
||||
),
|
||||
[RESULT_TYPES.samples]: useFilteredTableData(
|
||||
filterText,
|
||||
data[RESULT_TYPES.samples],
|
||||
),
|
||||
};
|
||||
|
||||
const columns = {
|
||||
[RESULT_TYPES.results]: useTableColumns(data[RESULT_TYPES.results]),
|
||||
[RESULT_TYPES.samples]: useTableColumns(data[RESULT_TYPES.samples]),
|
||||
};
|
||||
|
||||
const renderDataTable = (type: string) => {
|
||||
if (isLoading[type]) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (error[type]) {
|
||||
return <pre>{error[type]}</pre>;
|
||||
}
|
||||
if (data[type]) {
|
||||
if (data[type]?.length === 0) {
|
||||
return <span>No data</span>;
|
||||
}
|
||||
return (
|
||||
<TableView
|
||||
columns={columns[type]}
|
||||
data={filteredData[type]}
|
||||
withPagination={false}
|
||||
noDataText={t('No data')}
|
||||
emptyWrapperType={EmptyWrapperType.Small}
|
||||
className="table-condensed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const TableControls = (
|
||||
<TableControlsWrapper>
|
||||
<RowCount data={data[activeTabKey]} />
|
||||
<CopyToClipboardButton data={data[activeTabKey]} />
|
||||
<FilterInput onChangeHandler={setFilterText} />
|
||||
</TableControlsWrapper>
|
||||
);
|
||||
|
||||
const handleCollapseChange = (openPanelName: string) => {
|
||||
onCollapseChange(openPanelName);
|
||||
setPanelOpen(!!openPanelName);
|
||||
};
|
||||
|
||||
return (
|
||||
<SouthPane>
|
||||
{displayBackground && <SouthPaneBackground />}
|
||||
<TabsWrapper contentHeight={tableSectionHeight}>
|
||||
<Collapse accordion bordered={false} onChange={handleCollapseChange}>
|
||||
<Collapse.Panel header={t('Data')} key="data">
|
||||
<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>
|
||||
</TabsWrapper>
|
||||
</SouthPane>
|
||||
);
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
|
@ -26,25 +26,19 @@ import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/mar
|
|||
import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
|
||||
import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
|
||||
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
|
||||
import { DropdownButton, Row, Col, FormControl } from 'react-bootstrap';
|
||||
import { DropdownButton } from 'react-bootstrap';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
|
||||
import { Menu } from 'src/common/components';
|
||||
import TableView, { EmptyWrapperType } from 'src/components/TableView';
|
||||
import Button from 'src/components/Button';
|
||||
import getClientErrorObject from '../../utils/getClientErrorObject';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
import { getChartDataRequest } from '../../chart/chartAction';
|
||||
import downloadAsImage from '../../utils/downloadAsImage';
|
||||
import Loading from '../../components/Loading';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import RowCountLabel from './RowCountLabel';
|
||||
import {
|
||||
applyFormattingToTabularData,
|
||||
prepareCopyToClipboardTabularData,
|
||||
} from '../../utils/common';
|
||||
import PropertiesModal from './PropertiesModal';
|
||||
import { sliceUpdated } from '../actions/exploreActions';
|
||||
import { CopyButton } from './DataTableControl';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('markdown', markdownSyntax);
|
||||
SyntaxHighlighter.registerLanguage('html', htmlSyntax);
|
||||
|
|
@ -66,32 +60,9 @@ const MENU_KEYS = {
|
|||
DOWNLOAD_AS_IMAGE: 'download_as_image',
|
||||
};
|
||||
|
||||
const CopyButton = styled(Button)`
|
||||
padding: ${({ theme }) => theme.gridUnit / 2}px
|
||||
${({ theme }) => theme.gridUnit * 2.5}px;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
|
||||
// needed to override button's first-of-type margin: 0
|
||||
const CopyButtonViewQuery = styled(CopyButton)`
|
||||
&& {
|
||||
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
|
||||
i {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const CopyButtonViewQuery = styled(Button)`
|
||||
padding: ${({ theme }) => theme.gridUnit / 2}px
|
||||
${({ theme }) => theme.gridUnit * 2.5}px;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
|
||||
&& {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
padding: 0;
|
||||
margin: 0 0 ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -100,36 +71,14 @@ export const DisplayQueryButton = props => {
|
|||
|
||||
const [language, setLanguage] = useState(null);
|
||||
const [query, setQuery] = useState(null);
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [sqlSupported] = useState(
|
||||
datasource && datasource.split('__')[1] === 'table',
|
||||
);
|
||||
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
if (!data?.length) {
|
||||
return [];
|
||||
}
|
||||
const formattedData = applyFormattingToTabularData(data);
|
||||
return formattedData.filter(row =>
|
||||
Object.values(row).some(value =>
|
||||
value.toString().toLowerCase().includes(filterText.toLowerCase()),
|
||||
),
|
||||
);
|
||||
}, [data, filterText]);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
data?.length
|
||||
? Object.keys(data[0]).map(key => ({ accessor: key, Header: key }))
|
||||
: [],
|
||||
[data],
|
||||
);
|
||||
|
||||
const beforeOpen = resultType => {
|
||||
setIsLoading(true);
|
||||
|
||||
|
|
@ -139,11 +88,10 @@ export const DisplayQueryButton = props => {
|
|||
resultType,
|
||||
})
|
||||
.then(response => {
|
||||
// Currently displaying of only first query is supported
|
||||
// Only displaying the first query is currently supported
|
||||
const result = response.result[0];
|
||||
setLanguage(result.language);
|
||||
setQuery(result.query);
|
||||
setData(result.data);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
})
|
||||
|
|
@ -155,10 +103,6 @@ export const DisplayQueryButton = props => {
|
|||
});
|
||||
};
|
||||
|
||||
const changeFilterText = event => {
|
||||
setFilterText(event.target.value);
|
||||
};
|
||||
|
||||
const openPropertiesModal = () => {
|
||||
setIsPropertiesModalOpen(true);
|
||||
};
|
||||
|
|
@ -206,7 +150,7 @@ export const DisplayQueryButton = props => {
|
|||
text={query}
|
||||
shouldShowText={false}
|
||||
copyNode={
|
||||
<CopyButtonViewQuery>
|
||||
<CopyButtonViewQuery buttonSize="xs">
|
||||
<i className="fa fa-clipboard" />
|
||||
</CopyButtonViewQuery>
|
||||
}
|
||||
|
|
@ -220,76 +164,6 @@ export const DisplayQueryButton = props => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const renderDataTable = () => {
|
||||
return (
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
<Row>
|
||||
<Col md={9}>
|
||||
<RowCountLabel
|
||||
rowcount={data.length}
|
||||
suffix={t('rows retrieved')}
|
||||
/>
|
||||
<CopyToClipboard
|
||||
text={prepareCopyToClipboardTabularData(data)}
|
||||
wrapped={false}
|
||||
copyNode={
|
||||
<CopyButton>
|
||||
<i className="fa fa-clipboard" />
|
||||
</CopyButton>
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={3}>
|
||||
<FormControl
|
||||
placeholder={t('Search')}
|
||||
bsSize="sm"
|
||||
value={filterText}
|
||||
onChange={changeFilterText}
|
||||
style={{ paddingBottom: '5px' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<TableView
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
withPagination={false}
|
||||
noDataText={t('No data')}
|
||||
emptyWrapperType={EmptyWrapperType.Small}
|
||||
className="table-condensed"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderResultsModalBody = () => {
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (error) {
|
||||
return <pre>{error}</pre>;
|
||||
}
|
||||
if (data) {
|
||||
if (data.length === 0) {
|
||||
return 'No data';
|
||||
}
|
||||
return renderDataTable();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderSamplesModalBody = () => {
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (error) {
|
||||
return <pre>{error}</pre>;
|
||||
}
|
||||
if (data) {
|
||||
return renderDataTable();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const { slice } = props;
|
||||
return (
|
||||
<DropdownButton
|
||||
|
|
@ -330,24 +204,6 @@ export const DisplayQueryButton = props => {
|
|||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<ModalTrigger
|
||||
triggerNode={<span>{t('View results')}</span>}
|
||||
modalTitle={t('View results')}
|
||||
beforeOpen={() => beforeOpen('results')}
|
||||
modalBody={renderResultsModalBody()}
|
||||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<ModalTrigger
|
||||
triggerNode={<span>{t('View samples')}</span>}
|
||||
modalTitle={t('View samples')}
|
||||
beforeOpen={() => beforeOpen('samples')}
|
||||
modalBody={renderSamplesModalBody()}
|
||||
responsive
|
||||
/>
|
||||
</Menu.Item>
|
||||
{sqlSupported && (
|
||||
<Menu.Item key={MENU_KEYS.RUN_IN_SQL_LAB}>
|
||||
{t('Run in SQL Lab')}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,16 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Split from 'react-split';
|
||||
import { ParentSize } from '@vx/responsive';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { chartPropShape } from '../../dashboard/util/propShapes';
|
||||
import ChartContainer from '../../chart/ChartContainer';
|
||||
import { styled, useTheme } from '@superset-ui/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||
import ChartContainer from 'src/chart/ChartContainer';
|
||||
import ConnectedExploreChartHeader from './ExploreChartHeader';
|
||||
import { DataTablesPane } from './DataTablesPane';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
|
|
@ -49,22 +52,142 @@ const propTypes = {
|
|||
triggerRender: PropTypes.bool,
|
||||
};
|
||||
|
||||
const GUTTER_SIZE_FACTOR = 1.25;
|
||||
|
||||
const CHART_PANEL_PADDING = 30;
|
||||
|
||||
const INITIAL_SIZES = [90, 10];
|
||||
const MIN_SIZES = [300, 50];
|
||||
const DEFAULT_SOUTH_PANE_HEIGHT_PERCENT = 40;
|
||||
|
||||
const Styles = styled.div`
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
align-content: stretch;
|
||||
overflow: hidden;
|
||||
|
||||
& > div:last-of-type {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
width: ${({ theme }) => theme.gridUnit * 9}px;
|
||||
margin: ${({ theme }) => theme.gridUnit * GUTTER_SIZE_FACTOR}px auto;
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.ant-collapse {
|
||||
height: 100%;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
.ant-collapse-item {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
.ant-collapse-content,
|
||||
.ant-collapse-content-box {
|
||||
height: 100%;
|
||||
}
|
||||
.ant-collapse-header {
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||
& > .ant-collapse-arrow {
|
||||
top: 5px; // not a theme variable, override necessary after setting paddings to 0 to center arrow
|
||||
}
|
||||
}
|
||||
.ant-tabs {
|
||||
height: 100%;
|
||||
.ant-tabs-nav {
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 5}px;
|
||||
margin: 0;
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
overflow: hidden;
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
class ExploreChartPanel extends React.PureComponent {
|
||||
renderChart() {
|
||||
const { chart } = this.props;
|
||||
const headerHeight = this.props.standalone ? 0 : 80;
|
||||
const ExploreChartPanel = props => {
|
||||
const theme = useTheme();
|
||||
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
|
||||
const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
|
||||
|
||||
const panelHeadingRef = useRef(null);
|
||||
const [headerHeight, setHeaderHeight] = useState(props.standalone ? 0 : 50);
|
||||
const [splitSizes, setSplitSizes] = useState(INITIAL_SIZES);
|
||||
|
||||
const calcSectionHeight = percent => {
|
||||
const containerHeight = parseInt(props.height, 10) - headerHeight - 30;
|
||||
return (
|
||||
(containerHeight * percent) / 100 - (gutterHeight / 2 + gutterMargin)
|
||||
);
|
||||
};
|
||||
|
||||
const [chartSectionHeight, setChartSectionHeight] = useState(
|
||||
calcSectionHeight(INITIAL_SIZES[0]) - CHART_PANEL_PADDING,
|
||||
);
|
||||
const [tableSectionHeight, setTableSectionHeight] = useState(
|
||||
calcSectionHeight(INITIAL_SIZES[1]),
|
||||
);
|
||||
const [displaySouthPaneBackground, setDisplaySouthPaneBackground] = useState(
|
||||
false,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const calcHeaderSize = debounce(() => {
|
||||
setHeaderHeight(
|
||||
props.standalone ? 0 : panelHeadingRef?.current?.offsetHeight,
|
||||
);
|
||||
}, 100);
|
||||
calcHeaderSize();
|
||||
document.addEventListener('resize', calcHeaderSize);
|
||||
return () => document.removeEventListener('resize', calcHeaderSize);
|
||||
}, [props.standalone]);
|
||||
|
||||
const recalcPanelSizes = ([northPercent, southPercent]) => {
|
||||
setChartSectionHeight(
|
||||
calcSectionHeight(northPercent) - CHART_PANEL_PADDING,
|
||||
);
|
||||
setTableSectionHeight(calcSectionHeight(southPercent));
|
||||
};
|
||||
|
||||
const onDragStart = () => {
|
||||
setDisplaySouthPaneBackground(true);
|
||||
};
|
||||
|
||||
const onDragEnd = sizes => {
|
||||
recalcPanelSizes(sizes);
|
||||
setDisplaySouthPaneBackground(false);
|
||||
};
|
||||
|
||||
const onCollapseChange = openPanelName => {
|
||||
let splitSizes;
|
||||
if (!openPanelName) {
|
||||
splitSizes = INITIAL_SIZES;
|
||||
} else {
|
||||
splitSizes = [
|
||||
100 - DEFAULT_SOUTH_PANE_HEIGHT_PERCENT,
|
||||
DEFAULT_SOUTH_PANE_HEIGHT_PERCENT,
|
||||
];
|
||||
}
|
||||
setSplitSizes(splitSizes);
|
||||
recalcPanelSizes(splitSizes);
|
||||
};
|
||||
|
||||
const renderChart = () => {
|
||||
const { chart } = props;
|
||||
|
||||
return (
|
||||
<ParentSize>
|
||||
|
|
@ -73,67 +196,89 @@ class ExploreChartPanel extends React.PureComponent {
|
|||
height > 0 && (
|
||||
<ChartContainer
|
||||
width={Math.floor(width)}
|
||||
height={parseInt(this.props.height, 10) - headerHeight}
|
||||
height={chartSectionHeight}
|
||||
annotationData={chart.annotationData}
|
||||
chartAlert={chart.chartAlert}
|
||||
chartStackTrace={chart.chartStackTrace}
|
||||
chartId={chart.id}
|
||||
chartStatus={chart.chartStatus}
|
||||
triggerRender={this.props.triggerRender}
|
||||
datasource={this.props.datasource}
|
||||
errorMessage={this.props.errorMessage}
|
||||
formData={this.props.form_data}
|
||||
onQuery={this.props.onQuery}
|
||||
owners={this.props?.slice?.owners}
|
||||
triggerRender={props.triggerRender}
|
||||
datasource={props.datasource}
|
||||
errorMessage={props.errorMessage}
|
||||
formData={props.form_data}
|
||||
onQuery={props.onQuery}
|
||||
owners={props?.slice?.owners}
|
||||
queryResponse={chart.queryResponse}
|
||||
refreshOverlayVisible={this.props.refreshOverlayVisible}
|
||||
setControlValue={this.props.actions.setControlValue}
|
||||
timeout={this.props.timeout}
|
||||
refreshOverlayVisible={props.refreshOverlayVisible}
|
||||
setControlValue={props.actions.setControlValue}
|
||||
timeout={props.timeout}
|
||||
triggerQuery={chart.triggerQuery}
|
||||
vizType={this.props.vizType}
|
||||
vizType={props.vizType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ParentSize>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.standalone) {
|
||||
// dom manipulation hack to get rid of the boostrap theme's body background
|
||||
const standaloneClass = 'background-transparent';
|
||||
const bodyClasses = document.body.className.split(' ');
|
||||
if (bodyClasses.indexOf(standaloneClass) === -1) {
|
||||
document.body.className += ` ${standaloneClass}`;
|
||||
}
|
||||
return this.renderChart();
|
||||
if (props.standalone) {
|
||||
// dom manipulation hack to get rid of the boostrap theme's body background
|
||||
const standaloneClass = 'background-transparent';
|
||||
const bodyClasses = document.body.className.split(' ');
|
||||
if (!bodyClasses.includes(standaloneClass)) {
|
||||
document.body.className += ` ${standaloneClass}`;
|
||||
}
|
||||
|
||||
const header = (
|
||||
<ConnectedExploreChartHeader
|
||||
actions={this.props.actions}
|
||||
addHistory={this.props.addHistory}
|
||||
can_overwrite={this.props.can_overwrite}
|
||||
can_download={this.props.can_download}
|
||||
chartHeight={this.props.height}
|
||||
isStarred={this.props.isStarred}
|
||||
slice={this.props.slice}
|
||||
sliceName={this.props.sliceName}
|
||||
table_name={this.props.table_name}
|
||||
form_data={this.props.form_data}
|
||||
timeout={this.props.timeout}
|
||||
chart={this.props.chart}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Styles className="panel panel-default chart-container">
|
||||
<div className="panel-heading">{header}</div>
|
||||
<div className="panel-body">{this.renderChart()}</div>
|
||||
</Styles>
|
||||
);
|
||||
return renderChart();
|
||||
}
|
||||
}
|
||||
|
||||
const header = (
|
||||
<ConnectedExploreChartHeader
|
||||
actions={props.actions}
|
||||
addHistory={props.addHistory}
|
||||
can_overwrite={props.can_overwrite}
|
||||
can_download={props.can_download}
|
||||
chartHeight={props.height}
|
||||
isStarred={props.isStarred}
|
||||
slice={props.slice}
|
||||
sliceName={props.sliceName}
|
||||
table_name={props.table_name}
|
||||
form_data={props.form_data}
|
||||
timeout={props.timeout}
|
||||
chart={props.chart}
|
||||
/>
|
||||
);
|
||||
|
||||
const elementStyle = (dimension, elementSize, gutterSize) => {
|
||||
return {
|
||||
[dimension]: `calc(${elementSize}% - ${gutterSize + gutterMargin}px)`,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Styles className="panel panel-default chart-container">
|
||||
<div className="panel-heading" ref={panelHeadingRef}>
|
||||
{header}
|
||||
</div>
|
||||
<Split
|
||||
sizes={splitSizes}
|
||||
minSize={MIN_SIZES}
|
||||
direction="vertical"
|
||||
gutterSize={gutterHeight}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
elementStyle={elementStyle}
|
||||
>
|
||||
<div className="panel-body">{renderChart()}</div>
|
||||
<DataTablesPane
|
||||
queryFormData={props.chart.latestQueryFormData}
|
||||
tableSectionHeight={tableSectionHeight}
|
||||
onCollapseChange={onCollapseChange}
|
||||
displayBackground={displaySouthPaneBackground}
|
||||
/>
|
||||
</Split>
|
||||
</Styles>
|
||||
);
|
||||
};
|
||||
|
||||
ExploreChartPanel.propTypes = propTypes;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue