perf: Optimize dashboard chart-related components (#31241)
This commit is contained in:
parent
3d3c09d299
commit
eab888c63a
|
|
@ -16,7 +16,14 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, ReactNode, useContext, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { css, getExtensionsRegistry, styled, t } from '@superset-ui/core';
|
||||
import { useUiConfig } from 'src/components/UiConfigContext';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
|
@ -34,7 +41,6 @@ import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
|||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
type SliceHeaderProps = SliceHeaderControlsProps & {
|
||||
innerRef?: string;
|
||||
updateSliceName?: (arg0: string) => void;
|
||||
editMode?: boolean;
|
||||
annotationQuery?: object;
|
||||
|
|
@ -122,176 +128,182 @@ const ChartHeaderStyles = styled.div`
|
|||
`}
|
||||
`;
|
||||
|
||||
const SliceHeader: FC<SliceHeaderProps> = ({
|
||||
innerRef = null,
|
||||
forceRefresh = () => ({}),
|
||||
updateSliceName = () => ({}),
|
||||
toggleExpandSlice = () => ({}),
|
||||
logExploreChart = () => ({}),
|
||||
logEvent,
|
||||
exportCSV = () => ({}),
|
||||
exportXLSX = () => ({}),
|
||||
editMode = false,
|
||||
annotationQuery = {},
|
||||
annotationError = {},
|
||||
cachedDttm = null,
|
||||
updatedDttm = null,
|
||||
isCached = [],
|
||||
isExpanded = false,
|
||||
sliceName = '',
|
||||
supersetCanExplore = false,
|
||||
supersetCanShare = false,
|
||||
supersetCanCSV = false,
|
||||
exportPivotCSV,
|
||||
exportFullCSV,
|
||||
exportFullXLSX,
|
||||
slice,
|
||||
componentId,
|
||||
dashboardId,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
handleToggleFullSize,
|
||||
isFullSize,
|
||||
chartStatus,
|
||||
formData,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
const SliceHeaderExtension = extensionsRegistry.get('dashboard.slice.header');
|
||||
const uiConfig = useUiConfig();
|
||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||
const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
// TODO: change to indicator field after it will be implemented
|
||||
const crossFilterValue = useSelector<RootState, any>(
|
||||
state => state.dataMask[slice?.slice_id]?.filterState?.value,
|
||||
);
|
||||
const isCrossFiltersEnabled = useSelector<RootState, boolean>(
|
||||
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
|
||||
);
|
||||
const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
(
|
||||
{
|
||||
forceRefresh = () => ({}),
|
||||
updateSliceName = () => ({}),
|
||||
toggleExpandSlice = () => ({}),
|
||||
logExploreChart = () => ({}),
|
||||
logEvent,
|
||||
exportCSV = () => ({}),
|
||||
exportXLSX = () => ({}),
|
||||
editMode = false,
|
||||
annotationQuery = {},
|
||||
annotationError = {},
|
||||
cachedDttm = null,
|
||||
updatedDttm = null,
|
||||
isCached = [],
|
||||
isExpanded = false,
|
||||
sliceName = '',
|
||||
supersetCanExplore = false,
|
||||
supersetCanShare = false,
|
||||
supersetCanCSV = false,
|
||||
exportPivotCSV,
|
||||
exportFullCSV,
|
||||
exportFullXLSX,
|
||||
slice,
|
||||
componentId,
|
||||
dashboardId,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
handleToggleFullSize,
|
||||
isFullSize,
|
||||
chartStatus,
|
||||
formData,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const SliceHeaderExtension = extensionsRegistry.get(
|
||||
'dashboard.slice.header',
|
||||
);
|
||||
const uiConfig = useUiConfig();
|
||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||
const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
// TODO: change to indicator field after it will be implemented
|
||||
const crossFilterValue = useSelector<RootState, any>(
|
||||
state => state.dataMask[slice?.slice_id]?.filterState?.value,
|
||||
);
|
||||
const isCrossFiltersEnabled = useSelector<RootState, boolean>(
|
||||
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
|
||||
);
|
||||
|
||||
const canExplore = !editMode && supersetCanExplore;
|
||||
const canExplore = !editMode && supersetCanExplore;
|
||||
|
||||
useEffect(() => {
|
||||
const headerElement = headerRef.current;
|
||||
if (canExplore) {
|
||||
setHeaderTooltip(getSliceHeaderTooltip(sliceName));
|
||||
} else if (
|
||||
headerElement &&
|
||||
(headerElement.scrollWidth > headerElement.offsetWidth ||
|
||||
headerElement.scrollHeight > headerElement.offsetHeight)
|
||||
) {
|
||||
setHeaderTooltip(sliceName ?? null);
|
||||
} else {
|
||||
setHeaderTooltip(null);
|
||||
}
|
||||
}, [sliceName, width, height, canExplore]);
|
||||
useEffect(() => {
|
||||
const headerElement = headerRef.current;
|
||||
if (canExplore) {
|
||||
setHeaderTooltip(getSliceHeaderTooltip(sliceName));
|
||||
} else if (
|
||||
headerElement &&
|
||||
(headerElement.scrollWidth > headerElement.offsetWidth ||
|
||||
headerElement.scrollHeight > headerElement.offsetHeight)
|
||||
) {
|
||||
setHeaderTooltip(sliceName ?? null);
|
||||
} else {
|
||||
setHeaderTooltip(null);
|
||||
}
|
||||
}, [sliceName, width, height, canExplore]);
|
||||
|
||||
const exploreUrl = `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
|
||||
const exploreUrl = `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
|
||||
|
||||
return (
|
||||
<ChartHeaderStyles data-test="slice-header" ref={innerRef}>
|
||||
<div className="header-title" ref={headerRef}>
|
||||
<Tooltip title={headerTooltip}>
|
||||
<EditableTitle
|
||||
title={
|
||||
sliceName ||
|
||||
(editMode
|
||||
? '---' // this makes an empty title clickable
|
||||
: '')
|
||||
}
|
||||
canEdit={editMode}
|
||||
onSaveTitle={updateSliceName}
|
||||
showTooltip={false}
|
||||
url={canExplore ? exploreUrl : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!!Object.values(annotationQuery).length && (
|
||||
<Tooltip
|
||||
id="annotations-loading-tooltip"
|
||||
placement="top"
|
||||
title={annotationsLoading}
|
||||
>
|
||||
<i
|
||||
role="img"
|
||||
aria-label={annotationsLoading}
|
||||
className="fa fa-refresh warning"
|
||||
return (
|
||||
<ChartHeaderStyles data-test="slice-header" ref={ref}>
|
||||
<div className="header-title" ref={headerRef}>
|
||||
<Tooltip title={headerTooltip}>
|
||||
<EditableTitle
|
||||
title={
|
||||
sliceName ||
|
||||
(editMode
|
||||
? '---' // this makes an empty title clickable
|
||||
: '')
|
||||
}
|
||||
canEdit={editMode}
|
||||
onSaveTitle={updateSliceName}
|
||||
showTooltip={false}
|
||||
url={canExplore ? exploreUrl : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!!Object.values(annotationError).length && (
|
||||
<Tooltip
|
||||
id="annotation-errors-tooltip"
|
||||
placement="top"
|
||||
title={annotationsError}
|
||||
>
|
||||
<i
|
||||
role="img"
|
||||
aria-label={annotationsError}
|
||||
className="fa fa-exclamation-circle danger"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-controls">
|
||||
{!editMode && (
|
||||
<>
|
||||
{SliceHeaderExtension && (
|
||||
<SliceHeaderExtension
|
||||
sliceId={slice.slice_id}
|
||||
dashboardId={dashboardId}
|
||||
{!!Object.values(annotationQuery).length && (
|
||||
<Tooltip
|
||||
id="annotations-loading-tooltip"
|
||||
placement="top"
|
||||
title={annotationsLoading}
|
||||
>
|
||||
<i
|
||||
role="img"
|
||||
aria-label={annotationsLoading}
|
||||
className="fa fa-refresh warning"
|
||||
/>
|
||||
)}
|
||||
{crossFilterValue && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={t(
|
||||
'This chart applies cross-filters to charts whose datasets contain columns with the same name.',
|
||||
)}
|
||||
>
|
||||
<CrossFilterIcon iconSize="m" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!uiConfig.hideChartControls && (
|
||||
<FiltersBadge chartId={slice.slice_id} />
|
||||
)}
|
||||
{!uiConfig.hideChartControls && (
|
||||
<SliceHeaderControls
|
||||
slice={slice}
|
||||
isCached={isCached}
|
||||
isExpanded={isExpanded}
|
||||
cachedDttm={cachedDttm}
|
||||
updatedDttm={updatedDttm}
|
||||
toggleExpandSlice={toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
logExploreChart={logExploreChart}
|
||||
logEvent={logEvent}
|
||||
exportCSV={exportCSV}
|
||||
exportPivotCSV={exportPivotCSV}
|
||||
exportFullCSV={exportFullCSV}
|
||||
exportXLSX={exportXLSX}
|
||||
exportFullXLSX={exportFullXLSX}
|
||||
supersetCanExplore={supersetCanExplore}
|
||||
supersetCanShare={supersetCanShare}
|
||||
supersetCanCSV={supersetCanCSV}
|
||||
componentId={componentId}
|
||||
dashboardId={dashboardId}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
handleToggleFullSize={handleToggleFullSize}
|
||||
isFullSize={isFullSize}
|
||||
isDescriptionExpanded={isExpanded}
|
||||
chartStatus={chartStatus}
|
||||
formData={formData}
|
||||
exploreUrl={exploreUrl}
|
||||
crossFiltersEnabled={isCrossFiltersEnabled}
|
||||
</Tooltip>
|
||||
)}
|
||||
{!!Object.values(annotationError).length && (
|
||||
<Tooltip
|
||||
id="annotation-errors-tooltip"
|
||||
placement="top"
|
||||
title={annotationsError}
|
||||
>
|
||||
<i
|
||||
role="img"
|
||||
aria-label={annotationsError}
|
||||
className="fa fa-exclamation-circle danger"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ChartHeaderStyles>
|
||||
);
|
||||
};
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-controls">
|
||||
{!editMode && (
|
||||
<>
|
||||
{SliceHeaderExtension && (
|
||||
<SliceHeaderExtension
|
||||
sliceId={slice.slice_id}
|
||||
dashboardId={dashboardId}
|
||||
/>
|
||||
)}
|
||||
{crossFilterValue && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={t(
|
||||
'This chart applies cross-filters to charts whose datasets contain columns with the same name.',
|
||||
)}
|
||||
>
|
||||
<CrossFilterIcon iconSize="m" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!uiConfig.hideChartControls && (
|
||||
<FiltersBadge chartId={slice.slice_id} />
|
||||
)}
|
||||
{!uiConfig.hideChartControls && (
|
||||
<SliceHeaderControls
|
||||
slice={slice}
|
||||
isCached={isCached}
|
||||
isExpanded={isExpanded}
|
||||
cachedDttm={cachedDttm}
|
||||
updatedDttm={updatedDttm}
|
||||
toggleExpandSlice={toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
logExploreChart={logExploreChart}
|
||||
logEvent={logEvent}
|
||||
exportCSV={exportCSV}
|
||||
exportPivotCSV={exportPivotCSV}
|
||||
exportFullCSV={exportFullCSV}
|
||||
exportXLSX={exportXLSX}
|
||||
exportFullXLSX={exportFullXLSX}
|
||||
supersetCanExplore={supersetCanExplore}
|
||||
supersetCanShare={supersetCanShare}
|
||||
supersetCanCSV={supersetCanCSV}
|
||||
componentId={componentId}
|
||||
dashboardId={dashboardId}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
handleToggleFullSize={handleToggleFullSize}
|
||||
isFullSize={isFullSize}
|
||||
isDescriptionExpanded={isExpanded}
|
||||
chartStatus={chartStatus}
|
||||
formData={formData}
|
||||
exploreUrl={exploreUrl}
|
||||
crossFiltersEnabled={isCrossFiltersEnabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ChartHeaderStyles>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default SliceHeader;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import Popover, { PopoverProps } from 'src/components/Popover';
|
|||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import { getDashboardPermalink } from 'src/utils/urlUtils';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
|
||||
export type URLShortLinkButtonProps = {
|
||||
|
|
@ -42,10 +42,13 @@ export default function URLShortLinkButton({
|
|||
}: URLShortLinkButtonProps) {
|
||||
const [shortUrl, setShortUrl] = useState('');
|
||||
const { addDangerToast } = useToasts();
|
||||
const { dataMask, activeTabs } = useSelector((state: RootState) => ({
|
||||
dataMask: state.dataMask,
|
||||
activeTabs: state.dashboardState.activeTabs,
|
||||
}));
|
||||
const { dataMask, activeTabs } = useSelector(
|
||||
(state: RootState) => ({
|
||||
dataMask: state.dataMask,
|
||||
activeTabs: state.dashboardState.activeTabs,
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
const getCopyUrl = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
import cx from 'classnames';
|
||||
import { Component } from 'react';
|
||||
import { useCallback, useEffect, useRef, useMemo, useState, memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled, t, logging } from '@superset-ui/core';
|
||||
import { debounce, isEqual } from 'lodash';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { debounce } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { exportChart, mountExploreUrl } from 'src/explore/exploreUtils';
|
||||
import ChartContainer from 'src/components/Chart/ChartContainer';
|
||||
|
|
@ -32,13 +34,30 @@ import {
|
|||
LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART,
|
||||
LOG_ACTIONS_FORCE_REFRESH_CHART,
|
||||
} from 'src/logger/LogUtils';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
|
||||
|
||||
import SliceHeader from '../SliceHeader';
|
||||
import MissingChart from '../MissingChart';
|
||||
import { slicePropShape, chartPropShape } from '../../util/propShapes';
|
||||
import {
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
} from '../../../components/MessageToasts/actions';
|
||||
import {
|
||||
setFocusedFilterField,
|
||||
toggleExpandSlice,
|
||||
unsetFocusedFilterField,
|
||||
} from '../../actions/dashboardState';
|
||||
import { changeFilter } from '../../actions/dashboardFilters';
|
||||
import { refreshChart } from '../../../components/Chart/chartAction';
|
||||
import { logEvent } from '../../../logger/actions';
|
||||
import {
|
||||
getActiveFilters,
|
||||
getAppliedFilterValues,
|
||||
} from '../../util/activeDashboardFilters';
|
||||
import getFormDataWithExtraFilters from '../../util/charts/getFormDataWithExtraFilters';
|
||||
import { PLACEHOLDER_DATASOURCE } from '../../constants';
|
||||
|
||||
const propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
|
|
@ -50,53 +69,15 @@ const propTypes = {
|
|||
isComponentVisible: PropTypes.bool,
|
||||
handleToggleFullSize: PropTypes.func.isRequired,
|
||||
setControlValue: PropTypes.func,
|
||||
|
||||
// from redux
|
||||
chart: chartPropShape.isRequired,
|
||||
formData: PropTypes.object.isRequired,
|
||||
labelsColor: PropTypes.object,
|
||||
labelsColorMap: PropTypes.object,
|
||||
datasource: PropTypes.object,
|
||||
slice: slicePropShape.isRequired,
|
||||
sliceName: PropTypes.string.isRequired,
|
||||
timeout: PropTypes.number.isRequired,
|
||||
maxRows: PropTypes.number.isRequired,
|
||||
// all active filter fields in dashboard
|
||||
filters: PropTypes.object.isRequired,
|
||||
refreshChart: PropTypes.func.isRequired,
|
||||
logEvent: PropTypes.func.isRequired,
|
||||
toggleExpandSlice: PropTypes.func.isRequired,
|
||||
changeFilter: PropTypes.func.isRequired,
|
||||
setFocusedFilterField: PropTypes.func.isRequired,
|
||||
unsetFocusedFilterField: PropTypes.func.isRequired,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
isCached: PropTypes.bool,
|
||||
supersetCanExplore: PropTypes.bool.isRequired,
|
||||
supersetCanShare: PropTypes.bool.isRequired,
|
||||
supersetCanCSV: PropTypes.bool.isRequired,
|
||||
addSuccessToast: PropTypes.func.isRequired,
|
||||
addDangerToast: PropTypes.func.isRequired,
|
||||
ownState: PropTypes.object,
|
||||
filterState: PropTypes.object,
|
||||
postTransformProps: PropTypes.func,
|
||||
datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
|
||||
isFullSize: PropTypes.bool,
|
||||
extraControls: PropTypes.object,
|
||||
isInView: PropTypes.bool,
|
||||
emitCrossFilters: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
isCached: false,
|
||||
isComponentVisible: true,
|
||||
};
|
||||
|
||||
// we use state + shouldComponentUpdate() logic to prevent perf-wrecking
|
||||
// resizing across all slices on a dashboard on every update
|
||||
const RESIZE_TIMEOUT = 500;
|
||||
const SHOULD_UPDATE_ON_PROP_CHANGES = Object.keys(propTypes).filter(
|
||||
prop =>
|
||||
prop !== 'width' && prop !== 'height' && prop !== 'isComponentVisible',
|
||||
);
|
||||
const DEFAULT_HEADER_HEIGHT = 22;
|
||||
|
||||
const ChartWrapper = styled.div`
|
||||
|
|
@ -121,429 +102,457 @@ const SliceContainer = styled.div`
|
|||
max-height: 100%;
|
||||
`;
|
||||
|
||||
class Chart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
descriptionHeight: 0,
|
||||
};
|
||||
const EMPTY_OBJECT = {};
|
||||
|
||||
this.changeFilter = this.changeFilter.bind(this);
|
||||
this.handleFilterMenuOpen = this.handleFilterMenuOpen.bind(this);
|
||||
this.handleFilterMenuClose = this.handleFilterMenuClose.bind(this);
|
||||
this.exportCSV = this.exportCSV.bind(this);
|
||||
this.exportPivotCSV = this.exportPivotCSV.bind(this);
|
||||
this.exportFullCSV = this.exportFullCSV.bind(this);
|
||||
this.exportXLSX = this.exportXLSX.bind(this);
|
||||
this.exportFullXLSX = this.exportFullXLSX.bind(this);
|
||||
this.forceRefresh = this.forceRefresh.bind(this);
|
||||
this.resize = debounce(this.resize.bind(this), RESIZE_TIMEOUT);
|
||||
this.setDescriptionRef = this.setDescriptionRef.bind(this);
|
||||
this.setHeaderRef = this.setHeaderRef.bind(this);
|
||||
this.getChartHeight = this.getChartHeight.bind(this);
|
||||
this.getDescriptionHeight = this.getDescriptionHeight.bind(this);
|
||||
}
|
||||
const Chart = props => {
|
||||
const dispatch = useDispatch();
|
||||
const descriptionRef = useRef(null);
|
||||
const headerRef = useRef(null);
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// this logic mostly pertains to chart resizing. we keep a copy of the dimensions in
|
||||
// state so that we can buffer component size updates and only update on the final call
|
||||
// which improves performance significantly
|
||||
if (
|
||||
nextState.width !== this.state.width ||
|
||||
nextState.height !== this.state.height ||
|
||||
nextState.descriptionHeight !== this.state.descriptionHeight ||
|
||||
!isEqual(nextProps.datasource, this.props.datasource)
|
||||
) {
|
||||
return true;
|
||||
const boundActionCreators = useMemo(
|
||||
() =>
|
||||
bindActionCreators(
|
||||
{
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
toggleExpandSlice,
|
||||
changeFilter,
|
||||
setFocusedFilterField,
|
||||
unsetFocusedFilterField,
|
||||
refreshChart,
|
||||
logEvent,
|
||||
},
|
||||
dispatch,
|
||||
),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const chart = useSelector(state => state.charts[props.id] || EMPTY_OBJECT);
|
||||
const slice = useSelector(
|
||||
state => state.sliceEntities.slices[props.id] || EMPTY_OBJECT,
|
||||
);
|
||||
const editMode = useSelector(state => state.dashboardState.editMode);
|
||||
const isExpanded = useSelector(
|
||||
state => !!state.dashboardState.expandedSlices[props.id],
|
||||
);
|
||||
const supersetCanExplore = useSelector(
|
||||
state => !!state.dashboardInfo.superset_can_explore,
|
||||
);
|
||||
const supersetCanShare = useSelector(
|
||||
state => !!state.dashboardInfo.superset_can_share,
|
||||
);
|
||||
const supersetCanCSV = useSelector(
|
||||
state => !!state.dashboardInfo.superset_can_csv,
|
||||
);
|
||||
const timeout = useSelector(
|
||||
state => state.dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
||||
);
|
||||
const emitCrossFilters = useSelector(
|
||||
state => !!state.dashboardInfo.crossFiltersEnabled,
|
||||
);
|
||||
const datasource = useSelector(
|
||||
state =>
|
||||
(chart &&
|
||||
chart.form_data &&
|
||||
state.datasources[chart.form_data.datasource]) ||
|
||||
PLACEHOLDER_DATASOURCE,
|
||||
);
|
||||
|
||||
const [descriptionHeight, setDescriptionHeight] = useState(0);
|
||||
const [height, setHeight] = useState(props.height);
|
||||
const [width, setWidth] = useState(props.width);
|
||||
const history = useHistory();
|
||||
const resize = useCallback(
|
||||
debounce(() => {
|
||||
const { width, height } = props;
|
||||
setHeight(height);
|
||||
setWidth(width);
|
||||
}, RESIZE_TIMEOUT),
|
||||
[props.width, props.height],
|
||||
);
|
||||
|
||||
const ownColorScheme = chart.form_data?.color_scheme;
|
||||
|
||||
const addFilter = useCallback(
|
||||
(newSelectedValues = {}) => {
|
||||
boundActionCreators.logEvent(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, {
|
||||
id: chart.id,
|
||||
columns: Object.keys(newSelectedValues).filter(
|
||||
key => newSelectedValues[key] !== null,
|
||||
),
|
||||
});
|
||||
boundActionCreators.changeFilter(chart.id, newSelectedValues);
|
||||
},
|
||||
[boundActionCreators.logEvent, boundActionCreators.changeFilter, chart.id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
const descriptionHeight =
|
||||
isExpanded && descriptionRef.current
|
||||
? descriptionRef.current?.offsetHeight
|
||||
: 0;
|
||||
setDescriptionHeight(descriptionHeight);
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
// allow chart to update if the status changed and the previous status was loading.
|
||||
if (
|
||||
this.props?.chart?.chartStatus !== nextProps?.chart?.chartStatus &&
|
||||
this.props?.chart?.chartStatus === 'loading'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
useEffect(
|
||||
() => () => {
|
||||
resize.cancel();
|
||||
},
|
||||
[resize],
|
||||
);
|
||||
|
||||
// allow chart update/re-render only if visible:
|
||||
// under selected tab or no tab layout
|
||||
if (nextProps.isComponentVisible) {
|
||||
if (nextProps.chart.triggerQuery) {
|
||||
return true;
|
||||
}
|
||||
useEffect(() => {
|
||||
resize();
|
||||
}, [resize, props.isFullSize]);
|
||||
|
||||
if (nextProps.isFullSize !== this.props.isFullSize) {
|
||||
this.resize();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
nextProps.width !== this.props.width ||
|
||||
nextProps.height !== this.props.height ||
|
||||
nextProps.width !== this.state.width ||
|
||||
nextProps.height !== this.state.height
|
||||
) {
|
||||
this.resize();
|
||||
}
|
||||
|
||||
for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
|
||||
const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
|
||||
// use deep objects equality comparison to prevent
|
||||
// unnecessary updates when objects references change
|
||||
if (!areObjectsEqual(nextProps[prop], this.props[prop])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
// chart should re-render if color scheme or label colors were changed
|
||||
nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.label_colors || {},
|
||||
this.props.formData?.label_colors || {},
|
||||
) ||
|
||||
!areObjectsEqual(
|
||||
nextProps.formData?.map_label_colors || {},
|
||||
this.props.formData?.map_label_colors || {},
|
||||
) ||
|
||||
!isEqual(
|
||||
nextProps.formData?.shared_label_colors || [],
|
||||
this.props.formData?.shared_label_colors || [],
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `cacheBusterProp` is injected by react-hot-loader
|
||||
return this.props.cacheBusterProp !== nextProps.cacheBusterProp;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.isExpanded) {
|
||||
const descriptionHeight = this.getDescriptionHeight();
|
||||
this.setState({ descriptionHeight });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.resize.cancel();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isExpanded !== prevProps.isExpanded) {
|
||||
const descriptionHeight = this.getDescriptionHeight();
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ descriptionHeight });
|
||||
}
|
||||
}
|
||||
|
||||
getDescriptionHeight() {
|
||||
return this.props.isExpanded && this.descriptionRef
|
||||
? this.descriptionRef.offsetHeight
|
||||
: 0;
|
||||
}
|
||||
|
||||
getChartHeight() {
|
||||
const headerHeight = this.getHeaderHeight();
|
||||
return Math.max(
|
||||
this.state.height - headerHeight - this.state.descriptionHeight,
|
||||
20,
|
||||
);
|
||||
}
|
||||
|
||||
getHeaderHeight() {
|
||||
if (this.headerRef) {
|
||||
const computedStyle = getComputedStyle(this.headerRef).getPropertyValue(
|
||||
'margin-bottom',
|
||||
);
|
||||
const getHeaderHeight = useCallback(() => {
|
||||
if (headerRef.current) {
|
||||
const computedStyle = getComputedStyle(
|
||||
headerRef.current,
|
||||
).getPropertyValue('margin-bottom');
|
||||
const marginBottom = parseInt(computedStyle, 10) || 0;
|
||||
return this.headerRef.offsetHeight + marginBottom;
|
||||
return headerRef.current.offsetHeight + marginBottom;
|
||||
}
|
||||
return DEFAULT_HEADER_HEIGHT;
|
||||
}
|
||||
}, [headerRef]);
|
||||
|
||||
setDescriptionRef(ref) {
|
||||
this.descriptionRef = ref;
|
||||
}
|
||||
const getChartHeight = useCallback(() => {
|
||||
const headerHeight = getHeaderHeight();
|
||||
return Math.max(height - headerHeight - descriptionHeight, 20);
|
||||
}, [getHeaderHeight, height, descriptionHeight]);
|
||||
|
||||
setHeaderRef(ref) {
|
||||
this.headerRef = ref;
|
||||
}
|
||||
const handleFilterMenuOpen = useCallback(
|
||||
(chartId, column) => {
|
||||
boundActionCreators.setFocusedFilterField(chartId, column);
|
||||
},
|
||||
[boundActionCreators.setFocusedFilterField],
|
||||
);
|
||||
|
||||
resize() {
|
||||
const { width, height } = this.props;
|
||||
this.setState(() => ({ width, height }));
|
||||
}
|
||||
const handleFilterMenuClose = useCallback(
|
||||
(chartId, column) => {
|
||||
boundActionCreators.unsetFocusedFilterField(chartId, column);
|
||||
},
|
||||
[boundActionCreators.unsetFocusedFilterField],
|
||||
);
|
||||
|
||||
changeFilter(newSelectedValues = {}) {
|
||||
this.props.logEvent(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, {
|
||||
id: this.props.chart.id,
|
||||
columns: Object.keys(newSelectedValues).filter(
|
||||
key => newSelectedValues[key] !== null,
|
||||
),
|
||||
const logExploreChart = useCallback(() => {
|
||||
boundActionCreators.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, {
|
||||
slice_id: slice.slice_id,
|
||||
is_cached: props.isCached,
|
||||
});
|
||||
this.props.changeFilter(this.props.chart.id, newSelectedValues);
|
||||
}
|
||||
}, [boundActionCreators.logEvent, slice.slice_id, props.isCached]);
|
||||
|
||||
handleFilterMenuOpen(chartId, column) {
|
||||
this.props.setFocusedFilterField(chartId, column);
|
||||
}
|
||||
const chartConfiguration = useSelector(
|
||||
state => state.dashboardInfo.metadata?.chart_configuration,
|
||||
);
|
||||
const colorScheme = useSelector(state => state.dashboardState.colorScheme);
|
||||
const colorNamespace = useSelector(
|
||||
state => state.dashboardState.colorNamespace,
|
||||
);
|
||||
const datasetsStatus = useSelector(
|
||||
state => state.dashboardState.datasetsStatus,
|
||||
);
|
||||
const allSliceIds = useSelector(state => state.dashboardState.sliceIds);
|
||||
const nativeFilters = useSelector(state => state.nativeFilters?.filters);
|
||||
const dataMask = useSelector(state => state.dataMask);
|
||||
const labelsColor = useSelector(
|
||||
state => state.dashboardInfo?.metadata?.label_colors || EMPTY_OBJECT,
|
||||
);
|
||||
const labelsColorMap = useSelector(
|
||||
state => state.dashboardInfo?.metadata?.map_label_colors || EMPTY_OBJECT,
|
||||
);
|
||||
const sharedLabelsColors = useSelector(state =>
|
||||
enforceSharedLabelsColorsArray(
|
||||
state.dashboardInfo?.metadata?.shared_label_colors,
|
||||
),
|
||||
);
|
||||
|
||||
handleFilterMenuClose(chartId, column) {
|
||||
this.props.unsetFocusedFilterField(chartId, column);
|
||||
}
|
||||
|
||||
logExploreChart = () => {
|
||||
this.props.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, {
|
||||
slice_id: this.props.slice.slice_id,
|
||||
is_cached: this.props.isCached,
|
||||
});
|
||||
};
|
||||
|
||||
onExploreChart = async clickEvent => {
|
||||
const isOpenInNewTab =
|
||||
clickEvent.shiftKey || clickEvent.ctrlKey || clickEvent.metaKey;
|
||||
try {
|
||||
const lastTabId = window.localStorage.getItem('last_tab_id');
|
||||
const nextTabId = lastTabId
|
||||
? String(Number.parseInt(lastTabId, 10) + 1)
|
||||
: undefined;
|
||||
const key = await postFormData(
|
||||
this.props.datasource.id,
|
||||
this.props.datasource.type,
|
||||
this.props.formData,
|
||||
this.props.slice.slice_id,
|
||||
nextTabId,
|
||||
);
|
||||
const url = mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
[URL_PARAMS.sliceId.name]: this.props.slice.slice_id,
|
||||
});
|
||||
if (isOpenInNewTab) {
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
} else {
|
||||
this.props.history.push(url);
|
||||
}
|
||||
} catch (error) {
|
||||
logging.error(error);
|
||||
this.props.addDangerToast(t('An error occurred while opening Explore'));
|
||||
}
|
||||
};
|
||||
|
||||
exportFullCSV() {
|
||||
this.exportCSV(true);
|
||||
}
|
||||
|
||||
exportCSV(isFullCSV = false) {
|
||||
this.exportTable('csv', isFullCSV);
|
||||
}
|
||||
|
||||
exportPivotCSV() {
|
||||
this.exportTable('csv', false, true);
|
||||
}
|
||||
|
||||
exportXLSX() {
|
||||
this.exportTable('xlsx', false);
|
||||
}
|
||||
|
||||
exportFullXLSX() {
|
||||
this.exportTable('xlsx', true);
|
||||
}
|
||||
|
||||
exportTable(format, isFullCSV, isPivot = false) {
|
||||
const logAction =
|
||||
format === 'csv'
|
||||
? LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART
|
||||
: LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART;
|
||||
this.props.logEvent(logAction, {
|
||||
slice_id: this.props.slice.slice_id,
|
||||
is_cached: this.props.isCached,
|
||||
});
|
||||
exportChart({
|
||||
formData: isFullCSV
|
||||
? { ...this.props.formData, row_limit: this.props.maxRows }
|
||||
: this.props.formData,
|
||||
resultType: isPivot ? 'post_processed' : 'full',
|
||||
resultFormat: format,
|
||||
force: true,
|
||||
ownState: this.props.ownState,
|
||||
});
|
||||
}
|
||||
|
||||
forceRefresh() {
|
||||
this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_CHART, {
|
||||
slice_id: this.props.slice.slice_id,
|
||||
is_cached: this.props.isCached,
|
||||
});
|
||||
return this.props.refreshChart(
|
||||
this.props.chart.id,
|
||||
true,
|
||||
this.props.dashboardId,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
componentId,
|
||||
dashboardId,
|
||||
const formData = useMemo(
|
||||
() =>
|
||||
getFormDataWithExtraFilters({
|
||||
chart,
|
||||
chartConfiguration,
|
||||
filters: getAppliedFilterValues(props.id),
|
||||
colorScheme,
|
||||
colorNamespace,
|
||||
sliceId: props.id,
|
||||
nativeFilters,
|
||||
allSliceIds,
|
||||
dataMask,
|
||||
extraControls: props.extraControls,
|
||||
labelsColor,
|
||||
labelsColorMap,
|
||||
sharedLabelsColors,
|
||||
ownColorScheme,
|
||||
}),
|
||||
[
|
||||
chart,
|
||||
slice,
|
||||
datasource,
|
||||
isExpanded,
|
||||
editMode,
|
||||
filters,
|
||||
formData,
|
||||
chartConfiguration,
|
||||
props.id,
|
||||
props.extraControls,
|
||||
colorScheme,
|
||||
colorNamespace,
|
||||
nativeFilters,
|
||||
allSliceIds,
|
||||
dataMask,
|
||||
labelsColor,
|
||||
labelsColorMap,
|
||||
updateSliceName,
|
||||
sliceName,
|
||||
toggleExpandSlice,
|
||||
timeout,
|
||||
supersetCanExplore,
|
||||
supersetCanShare,
|
||||
supersetCanCSV,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
ownState,
|
||||
filterState,
|
||||
handleToggleFullSize,
|
||||
isFullSize,
|
||||
setControlValue,
|
||||
postTransformProps,
|
||||
datasetsStatus,
|
||||
isInView,
|
||||
emitCrossFilters,
|
||||
logEvent,
|
||||
} = this.props;
|
||||
sharedLabelsColors,
|
||||
ownColorScheme,
|
||||
],
|
||||
);
|
||||
|
||||
const { width } = this.state;
|
||||
// this prevents throwing in the case that a gridComponent
|
||||
// references a chart that is not associated with the dashboard
|
||||
if (!chart || !slice) {
|
||||
return <MissingChart height={this.getChartHeight()} />;
|
||||
}
|
||||
const onExploreChart = useCallback(
|
||||
async clickEvent => {
|
||||
const isOpenInNewTab =
|
||||
clickEvent.shiftKey || clickEvent.ctrlKey || clickEvent.metaKey;
|
||||
try {
|
||||
const lastTabId = window.localStorage.getItem('last_tab_id');
|
||||
const nextTabId = lastTabId
|
||||
? String(Number.parseInt(lastTabId, 10) + 1)
|
||||
: undefined;
|
||||
const key = await postFormData(
|
||||
datasource.id,
|
||||
datasource.type,
|
||||
formData,
|
||||
slice.slice_id,
|
||||
nextTabId,
|
||||
);
|
||||
const url = mountExploreUrl(null, {
|
||||
[URL_PARAMS.formDataKey.name]: key,
|
||||
[URL_PARAMS.sliceId.name]: slice.slice_id,
|
||||
});
|
||||
if (isOpenInNewTab) {
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
} else {
|
||||
history.push(url);
|
||||
}
|
||||
} catch (error) {
|
||||
logging.error(error);
|
||||
boundActionCreators.addDangerToast(
|
||||
t('An error occurred while opening Explore'),
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
datasource.id,
|
||||
datasource.type,
|
||||
formData,
|
||||
slice.slice_id,
|
||||
boundActionCreators.addDangerToast,
|
||||
history,
|
||||
],
|
||||
);
|
||||
|
||||
const { queriesResponse, chartUpdateEndTime, chartStatus } = chart;
|
||||
const isLoading = chartStatus === 'loading';
|
||||
const exportTable = useCallback(
|
||||
(format, isFullCSV, isPivot = false) => {
|
||||
const logAction =
|
||||
format === 'csv'
|
||||
? LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART
|
||||
: LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART;
|
||||
boundActionCreators.logEvent(logAction, {
|
||||
slice_id: slice.slice_id,
|
||||
is_cached: props.isCached,
|
||||
});
|
||||
exportChart({
|
||||
formData: isFullCSV
|
||||
? { ...formData, row_limit: props.maxRows }
|
||||
: formData,
|
||||
resultType: isPivot ? 'post_processed' : 'full',
|
||||
resultFormat: format,
|
||||
force: true,
|
||||
ownState: props.ownState,
|
||||
});
|
||||
},
|
||||
[
|
||||
slice.slice_id,
|
||||
props.isCached,
|
||||
formData,
|
||||
props.maxRows,
|
||||
props.ownState,
|
||||
boundActionCreators.logEvent,
|
||||
],
|
||||
);
|
||||
|
||||
const exportCSV = useCallback(
|
||||
(isFullCSV = false) => {
|
||||
exportTable('csv', isFullCSV);
|
||||
},
|
||||
[exportTable],
|
||||
);
|
||||
|
||||
const exportFullCSV = useCallback(() => {
|
||||
exportCSV(true);
|
||||
}, [exportCSV]);
|
||||
|
||||
const exportPivotCSV = useCallback(() => {
|
||||
exportTable('csv', false, true);
|
||||
}, [exportTable]);
|
||||
|
||||
const exportXLSX = useCallback(() => {
|
||||
exportTable('xlsx', false);
|
||||
}, [exportTable]);
|
||||
|
||||
const exportFullXLSX = useCallback(() => {
|
||||
exportTable('xlsx', true);
|
||||
}, [exportTable]);
|
||||
|
||||
const forceRefresh = useCallback(() => {
|
||||
boundActionCreators.logEvent(LOG_ACTIONS_FORCE_REFRESH_CHART, {
|
||||
slice_id: slice.slice_id,
|
||||
is_cached: props.isCached,
|
||||
});
|
||||
return boundActionCreators.refreshChart(chart.id, true, props.dashboardId);
|
||||
}, [
|
||||
boundActionCreators.refreshChart,
|
||||
chart.id,
|
||||
props.dashboardId,
|
||||
slice.slice_id,
|
||||
props.isCached,
|
||||
boundActionCreators.logEvent,
|
||||
]);
|
||||
|
||||
if (chart === EMPTY_OBJECT || slice === EMPTY_OBJECT) {
|
||||
return <MissingChart height={getChartHeight()} />;
|
||||
}
|
||||
|
||||
const { queriesResponse, chartUpdateEndTime, chartStatus, annotationQuery } =
|
||||
chart;
|
||||
const isLoading = chartStatus === 'loading';
|
||||
// eslint-disable-next-line camelcase
|
||||
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
||||
const cachedDttm =
|
||||
// eslint-disable-next-line camelcase
|
||||
const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
|
||||
const cachedDttm =
|
||||
// eslint-disable-next-line camelcase
|
||||
queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
|
||||
const initialValues = {};
|
||||
queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
|
||||
|
||||
return (
|
||||
<SliceContainer
|
||||
className="chart-slice"
|
||||
data-test="chart-grid-component"
|
||||
data-test-chart-id={id}
|
||||
data-test-viz-type={slice.viz_type}
|
||||
data-test-chart-name={slice.slice_name}
|
||||
>
|
||||
<SliceHeader
|
||||
innerRef={this.setHeaderRef}
|
||||
slice={slice}
|
||||
isExpanded={isExpanded}
|
||||
isCached={isCached}
|
||||
cachedDttm={cachedDttm}
|
||||
updatedDttm={chartUpdateEndTime}
|
||||
toggleExpandSlice={toggleExpandSlice}
|
||||
forceRefresh={this.forceRefresh}
|
||||
editMode={editMode}
|
||||
annotationQuery={chart.annotationQuery}
|
||||
logExploreChart={this.logExploreChart}
|
||||
logEvent={logEvent}
|
||||
onExploreChart={this.onExploreChart}
|
||||
exportCSV={this.exportCSV}
|
||||
exportPivotCSV={this.exportPivotCSV}
|
||||
exportXLSX={this.exportXLSX}
|
||||
exportFullCSV={this.exportFullCSV}
|
||||
exportFullXLSX={this.exportFullXLSX}
|
||||
updateSliceName={updateSliceName}
|
||||
sliceName={sliceName}
|
||||
supersetCanExplore={supersetCanExplore}
|
||||
supersetCanShare={supersetCanShare}
|
||||
supersetCanCSV={supersetCanCSV}
|
||||
componentId={componentId}
|
||||
dashboardId={dashboardId}
|
||||
filters={filters}
|
||||
addSuccessToast={addSuccessToast}
|
||||
addDangerToast={addDangerToast}
|
||||
handleToggleFullSize={handleToggleFullSize}
|
||||
isFullSize={isFullSize}
|
||||
chartStatus={chart.chartStatus}
|
||||
formData={formData}
|
||||
width={width}
|
||||
height={this.getHeaderHeight()}
|
||||
/>
|
||||
return (
|
||||
<SliceContainer
|
||||
className="chart-slice"
|
||||
data-test="chart-grid-component"
|
||||
data-test-chart-id={props.id}
|
||||
data-test-viz-type={slice.viz_type}
|
||||
data-test-chart-name={slice.slice_name}
|
||||
>
|
||||
<SliceHeader
|
||||
ref={headerRef}
|
||||
slice={slice}
|
||||
isExpanded={isExpanded}
|
||||
isCached={isCached}
|
||||
cachedDttm={cachedDttm}
|
||||
updatedDttm={chartUpdateEndTime}
|
||||
toggleExpandSlice={boundActionCreators.toggleExpandSlice}
|
||||
forceRefresh={forceRefresh}
|
||||
editMode={editMode}
|
||||
annotationQuery={annotationQuery}
|
||||
logExploreChart={logExploreChart}
|
||||
logEvent={boundActionCreators.logEvent}
|
||||
onExploreChart={onExploreChart}
|
||||
exportCSV={exportCSV}
|
||||
exportPivotCSV={exportPivotCSV}
|
||||
exportXLSX={exportXLSX}
|
||||
exportFullCSV={exportFullCSV}
|
||||
exportFullXLSX={exportFullXLSX}
|
||||
updateSliceName={props.updateSliceName}
|
||||
sliceName={props.sliceName}
|
||||
supersetCanExplore={supersetCanExplore}
|
||||
supersetCanShare={supersetCanShare}
|
||||
supersetCanCSV={supersetCanCSV}
|
||||
componentId={props.componentId}
|
||||
dashboardId={props.dashboardId}
|
||||
filters={getActiveFilters() || EMPTY_OBJECT}
|
||||
addSuccessToast={boundActionCreators.addSuccessToast}
|
||||
addDangerToast={boundActionCreators.addDangerToast}
|
||||
handleToggleFullSize={props.handleToggleFullSize}
|
||||
isFullSize={props.isFullSize}
|
||||
chartStatus={chartStatus}
|
||||
formData={formData}
|
||||
width={width}
|
||||
height={getHeaderHeight()}
|
||||
/>
|
||||
|
||||
{/*
|
||||
{/*
|
||||
This usage of dangerouslySetInnerHTML is safe since it is being used to render
|
||||
markdown that is sanitized with nh3. See:
|
||||
https://github.com/apache/superset/pull/4390
|
||||
and
|
||||
https://github.com/apache/superset/pull/23862
|
||||
*/}
|
||||
{isExpanded && slice.description_markeddown && (
|
||||
<div
|
||||
className="slice_description bs-callout bs-callout-default"
|
||||
ref={this.setDescriptionRef}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
|
||||
role="complementary"
|
||||
{isExpanded && slice.description_markeddown && (
|
||||
<div
|
||||
className="slice_description bs-callout bs-callout-default"
|
||||
ref={descriptionRef}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
|
||||
role="complementary"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChartWrapper
|
||||
className={cx('dashboard-chart')}
|
||||
aria-label={slice.description}
|
||||
>
|
||||
{isLoading && (
|
||||
<ChartOverlay
|
||||
style={{
|
||||
width,
|
||||
height: getChartHeight(),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChartWrapper
|
||||
className={cx('dashboard-chart')}
|
||||
aria-label={slice.description}
|
||||
>
|
||||
{isLoading && (
|
||||
<ChartOverlay
|
||||
style={{
|
||||
width,
|
||||
height: this.getChartHeight(),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChartContainer
|
||||
width={width}
|
||||
height={this.getChartHeight()}
|
||||
addFilter={this.changeFilter}
|
||||
onFilterMenuOpen={this.handleFilterMenuOpen}
|
||||
onFilterMenuClose={this.handleFilterMenuClose}
|
||||
annotationData={chart.annotationData}
|
||||
chartAlert={chart.chartAlert}
|
||||
chartId={id}
|
||||
chartStatus={chartStatus}
|
||||
datasource={datasource}
|
||||
dashboardId={dashboardId}
|
||||
initialValues={initialValues}
|
||||
formData={formData}
|
||||
labelsColor={labelsColor}
|
||||
labelsColorMap={labelsColorMap}
|
||||
ownState={ownState}
|
||||
filterState={filterState}
|
||||
queriesResponse={chart.queriesResponse}
|
||||
timeout={timeout}
|
||||
triggerQuery={chart.triggerQuery}
|
||||
vizType={slice.viz_type}
|
||||
setControlValue={setControlValue}
|
||||
postTransformProps={postTransformProps}
|
||||
datasetsStatus={datasetsStatus}
|
||||
isInView={isInView}
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</SliceContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
<ChartContainer
|
||||
width={width}
|
||||
height={getChartHeight()}
|
||||
addFilter={addFilter}
|
||||
onFilterMenuOpen={handleFilterMenuOpen}
|
||||
onFilterMenuClose={handleFilterMenuClose}
|
||||
annotationData={chart.annotationData}
|
||||
chartAlert={chart.chartAlert}
|
||||
chartId={props.id}
|
||||
chartStatus={chartStatus}
|
||||
datasource={datasource}
|
||||
dashboardId={props.dashboardId}
|
||||
initialValues={EMPTY_OBJECT}
|
||||
formData={formData}
|
||||
labelsColor={labelsColor}
|
||||
labelsColorMap={labelsColorMap}
|
||||
ownState={dataMask[props.id]?.ownState}
|
||||
filterState={dataMask[props.id]?.filterState}
|
||||
queriesResponse={chart.queriesResponse}
|
||||
timeout={timeout}
|
||||
triggerQuery={chart.triggerQuery}
|
||||
vizType={slice.viz_type}
|
||||
setControlValue={props.setControlValue}
|
||||
datasetsStatus={datasetsStatus}
|
||||
isInView={props.isInView}
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</SliceContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Chart.propTypes = propTypes;
|
||||
Chart.defaultProps = defaultProps;
|
||||
|
||||
export default withRouter(Chart);
|
||||
export default memo(Chart, (prevProps, nextProps) => {
|
||||
if (prevProps.cacheBusterProp !== nextProps.cacheBusterProp) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!nextProps.isComponentVisible ||
|
||||
(prevProps.isInView === nextProps.isInView &&
|
||||
prevProps.componentId === nextProps.componentId &&
|
||||
prevProps.id === nextProps.id &&
|
||||
prevProps.dashboardId === nextProps.dashboardId &&
|
||||
prevProps.extraControls === nextProps.extraControls &&
|
||||
prevProps.handleToggleFullSize === nextProps.handleToggleFullSize &&
|
||||
prevProps.isFullSize === nextProps.isFullSize &&
|
||||
prevProps.setControlValue === nextProps.setControlValue &&
|
||||
prevProps.sliceName === nextProps.sliceName &&
|
||||
prevProps.updateSliceName === nextProps.updateSliceName &&
|
||||
prevProps.width === nextProps.width &&
|
||||
prevProps.height === nextProps.height)
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
import { fireEvent, render } from 'spec/helpers/testing-library';
|
||||
import { FeatureFlag, VizType } from '@superset-ui/core';
|
||||
import * as redux from 'redux';
|
||||
|
||||
import Chart from 'src/dashboard/components/gridComponents/Chart';
|
||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||
|
|
@ -32,18 +33,10 @@ const props = {
|
|||
width: 100,
|
||||
height: 100,
|
||||
updateSliceName() {},
|
||||
|
||||
// from redux
|
||||
maxRows: 666,
|
||||
chart: chartQueries[queryId],
|
||||
formData: chartQueries[queryId].form_data,
|
||||
datasource: mockDatasource[sliceEntities.slices[queryId].datasource],
|
||||
slice: {
|
||||
...sliceEntities.slices[queryId],
|
||||
description_markeddown: 'markdown',
|
||||
owners: [],
|
||||
viz_type: VizType.Table,
|
||||
},
|
||||
sliceName: sliceEntities.slices[queryId].slice_name,
|
||||
timeout: 60,
|
||||
filters: {},
|
||||
|
|
@ -63,20 +56,60 @@ const props = {
|
|||
exportFullXLSX() {},
|
||||
componentId: 'test',
|
||||
dashboardId: 111,
|
||||
editMode: false,
|
||||
isExpanded: false,
|
||||
supersetCanExplore: false,
|
||||
supersetCanCSV: false,
|
||||
supersetCanShare: false,
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
return render(<Chart.WrappedComponent {...props} {...overrideProps} />, {
|
||||
const defaultState = {
|
||||
charts: chartQueries,
|
||||
sliceEntities: {
|
||||
...sliceEntities,
|
||||
slices: {
|
||||
[queryId]: {
|
||||
...sliceEntities.slices[queryId],
|
||||
description_markeddown: 'markdown',
|
||||
owners: [],
|
||||
viz_type: VizType.Table,
|
||||
},
|
||||
},
|
||||
},
|
||||
datasources: mockDatasource,
|
||||
dashboardState: { editMode: false, expandedSlices: {} },
|
||||
dashboardInfo: {
|
||||
superset_can_explore: false,
|
||||
superset_can_share: false,
|
||||
superset_can_csv: false,
|
||||
common: { conf: { SUPERSET_WEBSERVER_TIMEOUT: 0 } },
|
||||
},
|
||||
};
|
||||
|
||||
function setup(overrideProps, overrideState) {
|
||||
return render(<Chart {...props} {...overrideProps} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: { ...defaultState, ...overrideState },
|
||||
});
|
||||
}
|
||||
|
||||
const refreshChart = jest.fn();
|
||||
const logEvent = jest.fn();
|
||||
const changeFilter = jest.fn();
|
||||
const addSuccessToast = jest.fn();
|
||||
const addDangerToast = jest.fn();
|
||||
const toggleExpandSlice = jest.fn();
|
||||
const setFocusedFilterField = jest.fn();
|
||||
const unsetFocusedFilterField = jest.fn();
|
||||
beforeAll(() => {
|
||||
jest.spyOn(redux, 'bindActionCreators').mockImplementation(() => ({
|
||||
refreshChart,
|
||||
logEvent,
|
||||
changeFilter,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
toggleExpandSlice,
|
||||
setFocusedFilterField,
|
||||
unsetFocusedFilterField,
|
||||
}));
|
||||
});
|
||||
|
||||
test('should render a SliceHeader', () => {
|
||||
const { getByTestId, container } = setup();
|
||||
expect(getByTestId('slice-header')).toBeInTheDocument();
|
||||
|
|
@ -89,23 +122,20 @@ test('should render a ChartContainer', () => {
|
|||
});
|
||||
|
||||
test('should render a description if it has one and isExpanded=true', () => {
|
||||
const { container } = setup({ isExpanded: true });
|
||||
expect(container.querySelector('.slice_description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should calculate the description height if it has one and isExpanded=true', () => {
|
||||
const spy = jest.spyOn(
|
||||
Chart.WrappedComponent.prototype,
|
||||
'getDescriptionHeight',
|
||||
const { container } = setup(
|
||||
{},
|
||||
{
|
||||
dashboardState: {
|
||||
...defaultState.dashboardState,
|
||||
expandedSlices: { [props.id]: true },
|
||||
},
|
||||
},
|
||||
);
|
||||
const { container } = setup({ isExpanded: true });
|
||||
expect(container.querySelector('.slice_description')).toBeInTheDocument();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call refreshChart when SliceHeader calls forceRefresh', () => {
|
||||
const refreshChart = jest.fn();
|
||||
const { getByText, getByRole } = setup({ refreshChart });
|
||||
const { getByText, getByRole } = setup({});
|
||||
fireEvent.click(getByRole('button', { name: 'More Options' }));
|
||||
fireEvent.click(getByText('Force refresh'));
|
||||
expect(refreshChart).toHaveBeenCalled();
|
||||
|
|
@ -122,7 +152,12 @@ test('should call exportChart when exportCSV is clicked', async () => {
|
|||
const stubbedExportCSV = jest
|
||||
.spyOn(exploreUtils, 'exportChart')
|
||||
.mockImplementation(() => {});
|
||||
const { findByText, getByRole } = setup({ supersetCanCSV: true });
|
||||
const { findByText, getByRole } = setup(
|
||||
{},
|
||||
{
|
||||
dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
|
||||
},
|
||||
);
|
||||
fireEvent.click(getByRole('button', { name: 'More Options' }));
|
||||
fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
|
||||
const exportAction = await findByText('Export to .CSV');
|
||||
|
|
@ -145,7 +180,12 @@ test('should call exportChart with row_limit props.maxRows when exportFullCSV is
|
|||
const stubbedExportCSV = jest
|
||||
.spyOn(exploreUtils, 'exportChart')
|
||||
.mockImplementation(() => {});
|
||||
const { findByText, getByRole } = setup({ supersetCanCSV: true });
|
||||
const { findByText, getByRole } = setup(
|
||||
{},
|
||||
{
|
||||
dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
|
||||
},
|
||||
);
|
||||
fireEvent.click(getByRole('button', { name: 'More Options' }));
|
||||
fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
|
||||
const exportAction = await findByText('Export to full .CSV');
|
||||
|
|
@ -167,7 +207,12 @@ test('should call exportChart when exportXLSX is clicked', async () => {
|
|||
const stubbedExportXLSX = jest
|
||||
.spyOn(exploreUtils, 'exportChart')
|
||||
.mockImplementation(() => {});
|
||||
const { findByText, getByRole } = setup({ supersetCanCSV: true });
|
||||
const { findByText, getByRole } = setup(
|
||||
{},
|
||||
{
|
||||
dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
|
||||
},
|
||||
);
|
||||
fireEvent.click(getByRole('button', { name: 'More Options' }));
|
||||
fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
|
||||
const exportAction = await findByText('Export to Excel');
|
||||
|
|
@ -189,7 +234,12 @@ test('should call exportChart with row_limit props.maxRows when exportFullXLSX i
|
|||
const stubbedExportXLSX = jest
|
||||
.spyOn(exploreUtils, 'exportChart')
|
||||
.mockImplementation(() => {});
|
||||
const { findByText, getByRole } = setup({ supersetCanCSV: true });
|
||||
const { findByText, getByRole } = setup(
|
||||
{},
|
||||
{
|
||||
dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
|
||||
},
|
||||
);
|
||||
fireEvent.click(getByRole('button', { name: 'More Options' }));
|
||||
fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
|
||||
const exportAction = await findByText('Export to full Excel');
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect, memo } from 'react';
|
||||
|
||||
import { ResizeCallback, ResizeStartCallback } from 're-resizable';
|
||||
import cx from 'classnames';
|
||||
|
|
@ -24,7 +24,7 @@ import { useSelector } from 'react-redux';
|
|||
import { css, useTheme } from '@superset-ui/core';
|
||||
import { LayoutItem, RootState } from 'src/dashboard/types';
|
||||
import AnchorLink from 'src/dashboard/components/AnchorLink';
|
||||
import Chart from 'src/dashboard/containers/Chart';
|
||||
import Chart from 'src/dashboard/components/gridComponents/Chart';
|
||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||
import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
|
||||
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
|
||||
|
|
@ -70,7 +70,7 @@ interface ChartHolderProps {
|
|||
isInView: boolean;
|
||||
}
|
||||
|
||||
const ChartHolder: React.FC<ChartHolderProps> = ({
|
||||
const ChartHolder = ({
|
||||
id,
|
||||
parentId,
|
||||
component,
|
||||
|
|
@ -92,7 +92,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
|||
handleComponentDrop,
|
||||
setFullSizeChartId,
|
||||
isInView,
|
||||
}) => {
|
||||
}: ChartHolderProps) => {
|
||||
const theme = useTheme();
|
||||
const fullSizeStyle = css`
|
||||
&& {
|
||||
|
|
@ -107,9 +107,13 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
|||
const isFullSize = fullSizeChartId === chartId;
|
||||
|
||||
const focusHighlightStyles = useFilterFocusHighlightStyles(chartId);
|
||||
const dashboardState = useSelector(
|
||||
(state: RootState) => state.dashboardState,
|
||||
const directPathToChild = useSelector(
|
||||
(state: RootState) => state.dashboardState.directPathToChild,
|
||||
);
|
||||
const directPathLastUpdated = useSelector(
|
||||
(state: RootState) => state.dashboardState.directPathLastUpdated ?? 0,
|
||||
);
|
||||
|
||||
const [extraControls, setExtraControls] = useState<Record<string, unknown>>(
|
||||
{},
|
||||
);
|
||||
|
|
@ -118,18 +122,8 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
|||
const [currentDirectPathLastUpdated, setCurrentDirectPathLastUpdated] =
|
||||
useState(0);
|
||||
|
||||
const directPathToChild = useMemo(
|
||||
() => dashboardState?.directPathToChild ?? [],
|
||||
[dashboardState],
|
||||
);
|
||||
|
||||
const directPathLastUpdated = useMemo(
|
||||
() => dashboardState?.directPathLastUpdated ?? 0,
|
||||
[dashboardState],
|
||||
);
|
||||
|
||||
const infoFromPath = useMemo(
|
||||
() => getChartAndLabelComponentIdFromPath(directPathToChild) as any,
|
||||
() => getChartAndLabelComponentIdFromPath(directPathToChild ?? []) as any,
|
||||
[directPathToChild],
|
||||
);
|
||||
|
||||
|
|
@ -191,26 +185,26 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
|||
]);
|
||||
|
||||
const { chartWidth, chartHeight } = useMemo(() => {
|
||||
let chartWidth = 0;
|
||||
let chartHeight = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
if (isFullSize) {
|
||||
chartWidth = window.innerWidth - CHART_MARGIN;
|
||||
chartHeight = window.innerHeight - CHART_MARGIN;
|
||||
width = window.innerWidth - CHART_MARGIN;
|
||||
height = window.innerHeight - CHART_MARGIN;
|
||||
} else {
|
||||
chartWidth = Math.floor(
|
||||
width = Math.floor(
|
||||
widthMultiple * columnWidth +
|
||||
(widthMultiple - 1) * GRID_GUTTER_SIZE -
|
||||
CHART_MARGIN,
|
||||
);
|
||||
chartHeight = Math.floor(
|
||||
height = Math.floor(
|
||||
component.meta.height * GRID_BASE_UNIT - CHART_MARGIN,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
chartWidth: width,
|
||||
chartHeight: height,
|
||||
};
|
||||
}, [columnWidth, component, isFullSize, widthMultiple]);
|
||||
|
||||
|
|
@ -244,6 +238,111 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
|||
}));
|
||||
}, []);
|
||||
|
||||
const renderChild = useCallback(
|
||||
({ dragSourceRef }) => (
|
||||
<ResizableContainer
|
||||
id={component.id}
|
||||
adjustableWidth={parentComponent.type === ROW_TYPE}
|
||||
adjustableHeight
|
||||
widthStep={columnWidth}
|
||||
widthMultiple={widthMultiple}
|
||||
heightStep={GRID_BASE_UNIT}
|
||||
heightMultiple={component.meta.height}
|
||||
minWidthMultiple={GRID_MIN_COLUMN_COUNT}
|
||||
minHeightMultiple={GRID_MIN_ROW_UNITS}
|
||||
maxWidthMultiple={availableColumnCount + widthMultiple}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
editMode={editMode}
|
||||
>
|
||||
<div
|
||||
ref={dragSourceRef}
|
||||
data-test="dashboard-component-chart-holder"
|
||||
style={focusHighlightStyles}
|
||||
css={isFullSize ? fullSizeStyle : undefined}
|
||||
className={cx(
|
||||
'dashboard-component',
|
||||
'dashboard-component-chart-holder',
|
||||
// The following class is added to support custom dashboard styling via the CSS editor
|
||||
`dashboard-chart-id-${chartId}`,
|
||||
outlinedComponentId ? 'fade-in' : 'fade-out',
|
||||
)}
|
||||
>
|
||||
{!editMode && (
|
||||
<AnchorLink
|
||||
id={component.id}
|
||||
scrollIntoView={outlinedComponentId === component.id}
|
||||
/>
|
||||
)}
|
||||
{!!outlinedComponentId && (
|
||||
<style>
|
||||
{`label[for=${outlinedColumnName}] + .Select .Select__control {
|
||||
border-color: #00736a;
|
||||
transition: border-color 1s ease-in-out;
|
||||
}`}
|
||||
</style>
|
||||
)}
|
||||
<Chart
|
||||
componentId={component.id}
|
||||
id={component.meta.chartId}
|
||||
dashboardId={dashboardId}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
sliceName={
|
||||
component.meta.sliceNameOverride || component.meta.sliceName || ''
|
||||
}
|
||||
updateSliceName={handleUpdateSliceName}
|
||||
isComponentVisible={isComponentVisible}
|
||||
handleToggleFullSize={handleToggleFullSize}
|
||||
isFullSize={isFullSize}
|
||||
setControlValue={handleExtraControl}
|
||||
extraControls={extraControls}
|
||||
isInView={isInView}
|
||||
/>
|
||||
{editMode && (
|
||||
<HoverMenu position="top">
|
||||
<div data-test="dashboard-delete-component-button">
|
||||
<DeleteComponentButton onDelete={handleDeleteComponent} />
|
||||
</div>
|
||||
</HoverMenu>
|
||||
)}
|
||||
</div>
|
||||
</ResizableContainer>
|
||||
),
|
||||
[
|
||||
component.id,
|
||||
component.meta.height,
|
||||
component.meta.chartId,
|
||||
component.meta.sliceNameOverride,
|
||||
component.meta.sliceName,
|
||||
parentComponent.type,
|
||||
columnWidth,
|
||||
widthMultiple,
|
||||
availableColumnCount,
|
||||
onResizeStart,
|
||||
onResize,
|
||||
onResizeStop,
|
||||
editMode,
|
||||
focusHighlightStyles,
|
||||
isFullSize,
|
||||
fullSizeStyle,
|
||||
chartId,
|
||||
outlinedComponentId,
|
||||
outlinedColumnName,
|
||||
dashboardId,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
handleUpdateSliceName,
|
||||
isComponentVisible,
|
||||
handleToggleFullSize,
|
||||
handleExtraControl,
|
||||
extraControls,
|
||||
isInView,
|
||||
handleDeleteComponent,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
component={component}
|
||||
|
|
@ -255,81 +354,9 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
|
|||
disableDragDrop={false}
|
||||
editMode={editMode}
|
||||
>
|
||||
{({ dragSourceRef }) => (
|
||||
<ResizableContainer
|
||||
id={component.id}
|
||||
adjustableWidth={parentComponent.type === ROW_TYPE}
|
||||
adjustableHeight
|
||||
widthStep={columnWidth}
|
||||
widthMultiple={widthMultiple}
|
||||
heightStep={GRID_BASE_UNIT}
|
||||
heightMultiple={component.meta.height}
|
||||
minWidthMultiple={GRID_MIN_COLUMN_COUNT}
|
||||
minHeightMultiple={GRID_MIN_ROW_UNITS}
|
||||
maxWidthMultiple={availableColumnCount + widthMultiple}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
editMode={editMode}
|
||||
>
|
||||
<div
|
||||
ref={dragSourceRef}
|
||||
data-test="dashboard-component-chart-holder"
|
||||
style={focusHighlightStyles}
|
||||
css={isFullSize ? fullSizeStyle : undefined}
|
||||
className={cx(
|
||||
'dashboard-component',
|
||||
'dashboard-component-chart-holder',
|
||||
// The following class is added to support custom dashboard styling via the CSS editor
|
||||
`dashboard-chart-id-${chartId}`,
|
||||
outlinedComponentId ? 'fade-in' : 'fade-out',
|
||||
)}
|
||||
>
|
||||
{!editMode && (
|
||||
<AnchorLink
|
||||
id={component.id}
|
||||
scrollIntoView={outlinedComponentId === component.id}
|
||||
/>
|
||||
)}
|
||||
{!!outlinedComponentId && (
|
||||
<style>
|
||||
{`label[for=${outlinedColumnName}] + .Select .Select__control {
|
||||
border-color: #00736a;
|
||||
transition: border-color 1s ease-in-out;
|
||||
}`}
|
||||
</style>
|
||||
)}
|
||||
<Chart
|
||||
componentId={component.id}
|
||||
id={component.meta.chartId}
|
||||
dashboardId={dashboardId}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
sliceName={
|
||||
component.meta.sliceNameOverride ||
|
||||
component.meta.sliceName ||
|
||||
''
|
||||
}
|
||||
updateSliceName={handleUpdateSliceName}
|
||||
isComponentVisible={isComponentVisible}
|
||||
handleToggleFullSize={handleToggleFullSize}
|
||||
isFullSize={isFullSize}
|
||||
setControlValue={handleExtraControl}
|
||||
extraControls={extraControls}
|
||||
isInView={isInView}
|
||||
/>
|
||||
{editMode && (
|
||||
<HoverMenu position="top">
|
||||
<div data-test="dashboard-delete-component-button">
|
||||
<DeleteComponentButton onDelete={handleDeleteComponent} />
|
||||
</div>
|
||||
</HoverMenu>
|
||||
)}
|
||||
</div>
|
||||
</ResizableContainer>
|
||||
)}
|
||||
{renderChild}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartHolder;
|
||||
export default memo(ChartHolder);
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
/**
|
||||
* 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 { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
toggleExpandSlice,
|
||||
setFocusedFilterField,
|
||||
unsetFocusedFilterField,
|
||||
} from 'src/dashboard/actions/dashboardState';
|
||||
import { updateComponents } from 'src/dashboard/actions/dashboardLayout';
|
||||
import { changeFilter } from 'src/dashboard/actions/dashboardFilters';
|
||||
import {
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
} from 'src/components/MessageToasts/actions';
|
||||
import { refreshChart } from 'src/components/Chart/chartAction';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import {
|
||||
getActiveFilters,
|
||||
getAppliedFilterValues,
|
||||
} from 'src/dashboard/util/activeDashboardFilters';
|
||||
import getFormDataWithExtraFilters from 'src/dashboard/util/charts/getFormDataWithExtraFilters';
|
||||
import Chart from 'src/dashboard/components/gridComponents/Chart';
|
||||
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
|
||||
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
|
||||
|
||||
const EMPTY_OBJECT = {};
|
||||
|
||||
function mapStateToProps(
|
||||
{
|
||||
charts: chartQueries,
|
||||
dashboardInfo,
|
||||
dashboardState,
|
||||
dataMask,
|
||||
datasources,
|
||||
sliceEntities,
|
||||
nativeFilters,
|
||||
common,
|
||||
},
|
||||
ownProps,
|
||||
) {
|
||||
const { id, extraControls, setControlValue } = ownProps;
|
||||
const chart = chartQueries[id] || EMPTY_OBJECT;
|
||||
const datasource =
|
||||
(chart && chart.form_data && datasources[chart.form_data.datasource]) ||
|
||||
PLACEHOLDER_DATASOURCE;
|
||||
const {
|
||||
colorScheme: appliedColorScheme,
|
||||
colorNamespace,
|
||||
datasetsStatus,
|
||||
} = dashboardState;
|
||||
const labelsColor = dashboardInfo?.metadata?.label_colors || {};
|
||||
const labelsColorMap = dashboardInfo?.metadata?.map_label_colors || {};
|
||||
const sharedLabelsColors = enforceSharedLabelsColorsArray(
|
||||
dashboardInfo?.metadata?.shared_label_colors,
|
||||
);
|
||||
const ownColorScheme = chart.form_data?.color_scheme;
|
||||
// note: this method caches filters if possible to prevent render cascades
|
||||
const formData = getFormDataWithExtraFilters({
|
||||
chart,
|
||||
chartConfiguration: dashboardInfo.metadata?.chart_configuration,
|
||||
charts: chartQueries,
|
||||
filters: getAppliedFilterValues(id),
|
||||
colorNamespace,
|
||||
colorScheme: appliedColorScheme,
|
||||
ownColorScheme,
|
||||
sliceId: id,
|
||||
nativeFilters: nativeFilters?.filters,
|
||||
allSliceIds: dashboardState.sliceIds,
|
||||
dataMask,
|
||||
extraControls,
|
||||
labelsColor,
|
||||
labelsColorMap,
|
||||
sharedLabelsColors,
|
||||
});
|
||||
|
||||
formData.dashboardId = dashboardInfo.id;
|
||||
|
||||
return {
|
||||
chart,
|
||||
datasource,
|
||||
labelsColor,
|
||||
labelsColorMap,
|
||||
slice: sliceEntities.slices[id],
|
||||
timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
|
||||
filters: getActiveFilters() || EMPTY_OBJECT,
|
||||
formData,
|
||||
editMode: dashboardState.editMode,
|
||||
isExpanded: !!dashboardState.expandedSlices[id],
|
||||
supersetCanExplore: !!dashboardInfo.superset_can_explore,
|
||||
supersetCanShare: !!dashboardInfo.superset_can_share,
|
||||
supersetCanCSV: !!dashboardInfo.superset_can_csv,
|
||||
ownState: dataMask[id]?.ownState,
|
||||
filterState: dataMask[id]?.filterState,
|
||||
maxRows: common.conf.SQL_MAX_ROW,
|
||||
setControlValue,
|
||||
datasetsStatus,
|
||||
emitCrossFilters: !!dashboardInfo.crossFiltersEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
updateComponents,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
toggleExpandSlice,
|
||||
changeFilter,
|
||||
setFocusedFilterField,
|
||||
unsetFocusedFilterField,
|
||||
refreshChart,
|
||||
logEvent,
|
||||
},
|
||||
dispatch,
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Chart);
|
||||
Loading…
Reference in New Issue