mirror of
https://github.com/apache/superset.git
synced 2026-05-09 09:55:19 +00:00
330 lines
9.6 KiB
JavaScript
330 lines
9.6 KiB
JavaScript
/**
|
|
* 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, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import Split from 'react-split';
|
|
import { styled, SupersetClient, useTheme } from '@superset-ui/core';
|
|
import { useResizeDetector } from 'react-resize-detector';
|
|
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
|
import ChartContainer from 'src/chart/ChartContainer';
|
|
import {
|
|
getFromLocalStorage,
|
|
setInLocalStorage,
|
|
} from 'src/utils/localStorageHelpers';
|
|
import ConnectedExploreChartHeader from './ExploreChartHeader';
|
|
import { DataTablesPane } from './DataTablesPane';
|
|
import { buildV1ChartDataPayload } from '../exploreUtils';
|
|
|
|
const propTypes = {
|
|
actions: PropTypes.object.isRequired,
|
|
addHistory: PropTypes.func,
|
|
onQuery: PropTypes.func,
|
|
can_overwrite: PropTypes.bool.isRequired,
|
|
can_download: PropTypes.bool.isRequired,
|
|
datasource: PropTypes.object,
|
|
column_formats: PropTypes.object,
|
|
containerId: PropTypes.string.isRequired,
|
|
height: PropTypes.string.isRequired,
|
|
width: PropTypes.string.isRequired,
|
|
isStarred: PropTypes.bool.isRequired,
|
|
slice: PropTypes.object,
|
|
sliceName: PropTypes.string,
|
|
table_name: PropTypes.string,
|
|
vizType: PropTypes.string.isRequired,
|
|
form_data: PropTypes.object,
|
|
ownState: PropTypes.object,
|
|
standalone: PropTypes.number,
|
|
timeout: PropTypes.number,
|
|
refreshOverlayVisible: PropTypes.bool,
|
|
chart: chartPropShape,
|
|
errorMessage: PropTypes.node,
|
|
triggerRender: PropTypes.bool,
|
|
};
|
|
|
|
const GUTTER_SIZE_FACTOR = 1.25;
|
|
|
|
const CHART_PANEL_PADDING_HORIZ = 30;
|
|
const CHART_PANEL_PADDING_VERTICAL = 15;
|
|
const HEADER_PADDING = 15;
|
|
|
|
const STORAGE_KEYS = {
|
|
sizes: 'chart_split_sizes',
|
|
};
|
|
|
|
const INITIAL_SIZES = [90, 10];
|
|
const MIN_SIZES = [300, 50];
|
|
const DEFAULT_SOUTH_PANE_HEIGHT_PERCENT = 40;
|
|
|
|
const Styles = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
align-content: stretch;
|
|
overflow: auto;
|
|
box-shadow: none;
|
|
height: 100%;
|
|
|
|
& > 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 {
|
|
.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%;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const ExploreChartPanel = props => {
|
|
const theme = useTheme();
|
|
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
|
|
const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
|
|
|
|
const { height: hHeight, ref: headerRef } = useResizeDetector({
|
|
refreshMode: 'debounce',
|
|
refreshRate: 300,
|
|
});
|
|
const { width: chartPanelWidth, ref: chartPanelRef } = useResizeDetector({
|
|
refreshMode: 'debounce',
|
|
refreshRate: 300,
|
|
});
|
|
const [splitSizes, setSplitSizes] = useState(
|
|
getFromLocalStorage(STORAGE_KEYS.sizes, INITIAL_SIZES),
|
|
);
|
|
|
|
const { slice } = props;
|
|
const updateQueryContext = useCallback(
|
|
async function fetchChartData() {
|
|
if (slice && slice.query_context === null) {
|
|
const queryContext = buildV1ChartDataPayload({
|
|
formData: slice.form_data,
|
|
force: false,
|
|
resultFormat: 'json',
|
|
resultType: 'full',
|
|
setDataMask: null,
|
|
ownState: null,
|
|
});
|
|
|
|
await SupersetClient.put({
|
|
endpoint: `/api/v1/chart/${slice.slice_id}`,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
query_context: JSON.stringify(queryContext),
|
|
query_context_generation: true,
|
|
}),
|
|
});
|
|
}
|
|
},
|
|
[slice],
|
|
);
|
|
useEffect(() => {
|
|
updateQueryContext();
|
|
}, [updateQueryContext]);
|
|
|
|
const calcSectionHeight = useCallback(
|
|
percent => {
|
|
let headerHeight;
|
|
if (props.standalone) {
|
|
headerHeight = 0;
|
|
} else if (hHeight) {
|
|
headerHeight = hHeight + HEADER_PADDING;
|
|
} else {
|
|
headerHeight = 50;
|
|
}
|
|
const containerHeight = parseInt(props.height, 10) - headerHeight;
|
|
return (
|
|
(containerHeight * percent) / 100 - (gutterHeight / 2 + gutterMargin)
|
|
);
|
|
},
|
|
[gutterHeight, gutterMargin, props.height, props.standalone, hHeight],
|
|
);
|
|
|
|
const [tableSectionHeight, setTableSectionHeight] = useState(
|
|
calcSectionHeight(INITIAL_SIZES[1]),
|
|
);
|
|
|
|
const recalcPanelSizes = useCallback(
|
|
([, southPercent]) => {
|
|
setTableSectionHeight(calcSectionHeight(southPercent));
|
|
},
|
|
[calcSectionHeight],
|
|
);
|
|
|
|
useEffect(() => {
|
|
recalcPanelSizes(splitSizes);
|
|
}, [recalcPanelSizes, splitSizes]);
|
|
|
|
useEffect(() => {
|
|
setInLocalStorage(STORAGE_KEYS.sizes, splitSizes);
|
|
}, [splitSizes]);
|
|
|
|
const onDragEnd = sizes => {
|
|
setSplitSizes(sizes);
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
const renderChart = useCallback(() => {
|
|
const { chart, vizType } = props;
|
|
const newHeight =
|
|
vizType === 'filter_box'
|
|
? calcSectionHeight(100) - CHART_PANEL_PADDING_VERTICAL
|
|
: calcSectionHeight(splitSizes[0]) - CHART_PANEL_PADDING_VERTICAL;
|
|
const chartWidth = chartPanelWidth - CHART_PANEL_PADDING_HORIZ;
|
|
return (
|
|
chartWidth > 0 && (
|
|
<ChartContainer
|
|
width={Math.floor(chartWidth)}
|
|
height={newHeight}
|
|
ownState={props.ownState}
|
|
annotationData={chart.annotationData}
|
|
chartAlert={chart.chartAlert}
|
|
chartStackTrace={chart.chartStackTrace}
|
|
chartId={chart.id}
|
|
chartStatus={chart.chartStatus}
|
|
triggerRender={props.triggerRender}
|
|
datasource={props.datasource}
|
|
errorMessage={props.errorMessage}
|
|
formData={props.form_data}
|
|
onQuery={props.onQuery}
|
|
queriesResponse={chart.queriesResponse}
|
|
refreshOverlayVisible={props.refreshOverlayVisible}
|
|
setControlValue={props.actions.setControlValue}
|
|
timeout={props.timeout}
|
|
triggerQuery={chart.triggerQuery}
|
|
vizType={props.vizType}
|
|
/>
|
|
)
|
|
);
|
|
}, [calcSectionHeight, chartPanelWidth, props, splitSizes]);
|
|
|
|
const panelBody = useMemo(
|
|
() => (
|
|
<div className="panel-body" ref={chartPanelRef}>
|
|
{renderChart()}
|
|
</div>
|
|
),
|
|
[chartPanelRef, renderChart],
|
|
);
|
|
|
|
const standaloneChartBody = useMemo(
|
|
() => <div ref={chartPanelRef}>{renderChart()}</div>,
|
|
[chartPanelRef, 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}`;
|
|
}
|
|
return standaloneChartBody;
|
|
}
|
|
|
|
const header = (
|
|
<ConnectedExploreChartHeader
|
|
ownState={props.ownState}
|
|
actions={props.actions}
|
|
addHistory={props.addHistory}
|
|
can_overwrite={props.can_overwrite}
|
|
can_download={props.can_download}
|
|
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}
|
|
user={props.user}
|
|
reports={props.reports}
|
|
/>
|
|
);
|
|
|
|
const elementStyle = (dimension, elementSize, gutterSize) => ({
|
|
[dimension]: `calc(${elementSize}% - ${gutterSize + gutterMargin}px)`,
|
|
});
|
|
|
|
return (
|
|
<Styles className="panel panel-default chart-container" ref={chartPanelRef}>
|
|
<div className="panel-heading" ref={headerRef}>
|
|
{header}
|
|
</div>
|
|
{props.vizType === 'filter_box' ? (
|
|
panelBody
|
|
) : (
|
|
<Split
|
|
sizes={splitSizes}
|
|
minSize={MIN_SIZES}
|
|
direction="vertical"
|
|
gutterSize={gutterHeight}
|
|
onDragEnd={onDragEnd}
|
|
elementStyle={elementStyle}
|
|
>
|
|
{panelBody}
|
|
<DataTablesPane
|
|
ownState={props.ownState}
|
|
queryFormData={props.chart.latestQueryFormData}
|
|
tableSectionHeight={tableSectionHeight}
|
|
onCollapseChange={onCollapseChange}
|
|
chartStatus={props.chart.chartStatus}
|
|
errorMessage={props.errorMessage}
|
|
/>
|
|
</Split>
|
|
)}
|
|
</Styles>
|
|
);
|
|
};
|
|
|
|
ExploreChartPanel.propTypes = propTypes;
|
|
|
|
export default ExploreChartPanel;
|