From 41738df77d4b93f4a0af666dd7fcd82a7a92247f Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Sat, 5 Dec 2020 04:49:24 +0100 Subject: [PATCH] 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 --- .../explore/visualizations/big_number.test.js | 6 +- superset-frontend/package-lock.json | 8 +- superset-frontend/package.json | 2 +- .../components/DisplayQueryButton_spec.jsx | 4 +- superset-frontend/src/chart/Chart.jsx | 2 + .../explore/components/DataTableControl.tsx | 105 ++++++++ .../src/explore/components/DataTablesPane.tsx | 245 +++++++++++++++++ .../explore/components/DisplayQueryButton.jsx | 158 +---------- .../explore/components/ExploreChartPanel.jsx | 255 ++++++++++++++---- 9 files changed, 569 insertions(+), 216 deletions(-) create mode 100644 superset-frontend/src/explore/components/DataTableControl.tsx create mode 100644 superset-frontend/src/explore/components/DataTablesPane.tsx diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js index 6f6ed8d8e..fb7f15cd6 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js @@ -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'); }); }); diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index b428efe9c..18f81c153 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -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": { diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 435118ffd..762aa3918 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -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", diff --git a/superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx index 4d1bb4556..263656a6a 100644 --- a/superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx @@ -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); }); }); diff --git a/superset-frontend/src/chart/Chart.jsx b/superset-frontend/src/chart/Chart.jsx index 2636751fd..bb4910c56 100644 --- a/superset-frontend/src/chart/Chart.jsx +++ b/superset-frontend/src/chart/Chart.jsx @@ -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; diff --git a/superset-frontend/src/explore/components/DataTableControl.tsx b/superset-frontend/src/explore/components/DataTableControl.tsx new file mode 100644 index 000000000..8768e31bf --- /dev/null +++ b/superset-frontend/src/explore/components/DataTableControl.tsx @@ -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; +}) => ( + + + + } + /> +); + +export const FilterInput = ({ + onChangeHandler, +}: { + onChangeHandler(filterText: string): void; +}) => { + const debouncedChangeHandler = debounce(onChangeHandler, 500); + return ( + { + const filterText = event.target.value; + debouncedChangeHandler(filterText); + }} + /> + ); +}; + +export const RowCount = ({ data }: { data?: Record[] }) => ( + +); + +export const useFilteredTableData = ( + filterText: string, + data?: Record[], +) => + useMemo(() => { + if (!data?.length) { + return []; + } + const formattedData = applyFormattingToTabularData(data); + return formattedData.filter((row: Record) => + Object.values(row).some(value => + value.toString().toLowerCase().includes(filterText.toLowerCase()), + ), + ); + }, [data, filterText]); + +export const useTableColumns = (data?: Record[]) => + useMemo( + () => + data?.length + ? Object.keys(data[0]).map(key => ({ accessor: key, Header: key })) + : [], + [data], + ); diff --git a/superset-frontend/src/explore/components/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane.tsx new file mode 100644 index 000000000..b1bc9081d --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane.tsx @@ -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; + tableSectionHeight: number; + onCollapseChange: (openPanelName: string) => void; + displayBackground: boolean; +}) => { + const [data, setData] = useState<{ + [RESULT_TYPES.results]?: Record[]; + [RESULT_TYPES.samples]?: Record[]; + }>(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( + 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 ; + } + if (error[type]) { + return
{error[type]}
; + } + if (data[type]) { + if (data[type]?.length === 0) { + return No data; + } + return ( + + ); + } + return null; + }; + + const TableControls = ( + + + + + + ); + + const handleCollapseChange = (openPanelName: string) => { + onCollapseChange(openPanelName); + setPanelOpen(!!openPanelName); + }; + + return ( + + {displayBackground && } + + + + + + {renderDataTable(RESULT_TYPES.results)} + + + {renderDataTable(RESULT_TYPES.samples)} + + + + + + + ); +}; diff --git a/superset-frontend/src/explore/components/DisplayQueryButton.jsx b/superset-frontend/src/explore/components/DisplayQueryButton.jsx index 869674dc3..148215609 100644 --- a/superset-frontend/src/explore/components/DisplayQueryButton.jsx +++ b/superset-frontend/src/explore/components/DisplayQueryButton.jsx @@ -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={ - + } @@ -220,76 +164,6 @@ export const DisplayQueryButton = props => { return null; }; - const renderDataTable = () => { - return ( -
- - - - - - - } - /> - - - - - - -
- ); - }; - - const renderResultsModalBody = () => { - if (isLoading) { - return ; - } - if (error) { - return
{error}
; - } - if (data) { - if (data.length === 0) { - return 'No data'; - } - return renderDataTable(); - } - return null; - }; - - const renderSamplesModalBody = () => { - if (isLoading) { - return ; - } - if (error) { - return
{error}
; - } - if (data) { - return renderDataTable(); - } - return null; - }; - const { slice } = props; return ( { responsive /> - - {t('View results')}} - modalTitle={t('View results')} - beforeOpen={() => beforeOpen('results')} - modalBody={renderResultsModalBody()} - responsive - /> - - - {t('View samples')}} - modalTitle={t('View samples')} - beforeOpen={() => beforeOpen('samples')} - modalBody={renderSamplesModalBody()} - responsive - /> - {sqlSupported && ( {t('Run in SQL Lab')} diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx b/superset-frontend/src/explore/components/ExploreChartPanel.jsx index 674135e43..e90f4fe6d 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx @@ -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 ( @@ -73,67 +196,89 @@ class ExploreChartPanel extends React.PureComponent { height > 0 && ( ) } ); - } + }; - 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 = ( - - ); - - return ( - -
{header}
-
{this.renderChart()}
-
- ); + return renderChart(); } -} + + const header = ( + + ); + + const elementStyle = (dimension, elementSize, gutterSize) => { + return { + [dimension]: `calc(${elementSize}% - ${gutterSize + gutterMargin}px)`, + }; + }; + + return ( + +
+ {header} +
+ +
{renderChart()}
+ +
+
+ ); +}; ExploreChartPanel.propTypes = propTypes;