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:
Kamil Gabryjelski 2020-12-05 04:49:24 +01:00 committed by GitHub
parent 66cd565bff
commit 41738df77d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 569 additions and 216 deletions

View File

@ -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');
});
});

View File

@ -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": {

View File

@ -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",

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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],
);

View File

@ -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>
);
};

View File

@ -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')}

View File

@ -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;