mirror of
https://github.com/apache/superset.git
synced 2026-05-07 08:54:23 +00:00
759 lines
23 KiB
JavaScript
759 lines
23 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 cx from 'classnames';
|
|
import { useCallback, useEffect, useRef, useMemo, useState, memo } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { t, logging } from '@superset-ui/core';
|
|
import { styled } from '@apache-superset/core/ui';
|
|
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';
|
|
import {
|
|
StreamingExportModal,
|
|
useStreamingExport,
|
|
} from 'src/components/StreamingExportModal';
|
|
import {
|
|
LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
|
|
LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
|
|
LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
|
|
LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART,
|
|
LOG_ACTIONS_FORCE_REFRESH_CHART,
|
|
} from 'src/logger/LogUtils';
|
|
import { postFormData } from 'src/explore/exploreUtils/formData';
|
|
import { URL_PARAMS, DEFAULT_CSV_STREAMING_ROW_THRESHOLD } from 'src/constants';
|
|
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
|
|
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
|
|
import {
|
|
convertChartStateToOwnState,
|
|
hasChartStateConverter,
|
|
} from '../../../util/chartStateConverter';
|
|
|
|
import SliceHeader from '../../SliceHeader';
|
|
import MissingChart from '../../MissingChart';
|
|
import {
|
|
addDangerToast,
|
|
addSuccessToast,
|
|
} from '../../../../components/MessageToasts/actions';
|
|
import {
|
|
setFocusedFilterField,
|
|
toggleExpandSlice,
|
|
unsetFocusedFilterField,
|
|
updateChartState,
|
|
} 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,
|
|
componentId: PropTypes.string.isRequired,
|
|
dashboardId: PropTypes.number.isRequired,
|
|
width: PropTypes.number.isRequired,
|
|
height: PropTypes.number.isRequired,
|
|
updateSliceName: PropTypes.func.isRequired,
|
|
isComponentVisible: PropTypes.bool,
|
|
handleToggleFullSize: PropTypes.func.isRequired,
|
|
setControlValue: PropTypes.func,
|
|
sliceName: PropTypes.string.isRequired,
|
|
isFullSize: PropTypes.bool,
|
|
extraControls: PropTypes.object,
|
|
isInView: PropTypes.bool,
|
|
};
|
|
|
|
const RESIZE_TIMEOUT = 500;
|
|
const DEFAULT_HEADER_HEIGHT = 22;
|
|
|
|
const ChartWrapper = styled.div`
|
|
overflow: hidden;
|
|
position: relative;
|
|
|
|
&.dashboard-chart--overflowable {
|
|
overflow: visible;
|
|
}
|
|
`;
|
|
|
|
const ChartOverlay = styled.div`
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 5;
|
|
`;
|
|
|
|
const SliceContainer = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: 100%;
|
|
`;
|
|
|
|
const EMPTY_OBJECT = {};
|
|
const EMPTY_ARRAY = [];
|
|
|
|
// Helper function to get chart state with fallback
|
|
const getChartStateWithFallback = (chartState, formData, vizType) => {
|
|
if (!hasChartStateConverter(vizType)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
chartState?.state || formData.table_state || formData.pivot_table_state
|
|
);
|
|
};
|
|
|
|
// Helper function to create own state with chart state conversion
|
|
const createOwnStateWithChartState = (baseOwnState, chartState, vizType) => {
|
|
const state = getChartStateWithFallback(chartState, {}, vizType);
|
|
|
|
if (!state) {
|
|
return baseOwnState;
|
|
}
|
|
|
|
const convertedState = convertChartStateToOwnState(vizType, state);
|
|
return {
|
|
...baseOwnState,
|
|
...convertedState,
|
|
chartState: state,
|
|
};
|
|
};
|
|
|
|
const Chart = props => {
|
|
const dispatch = useDispatch();
|
|
const descriptionRef = useRef(null);
|
|
const headerRef = useRef(null);
|
|
|
|
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 { queriesResponse, chartUpdateEndTime, chartStatus, annotationQuery } =
|
|
chart;
|
|
|
|
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 maxRows = useSelector(
|
|
state => state.dashboardInfo.common.conf.SQL_MAX_ROW,
|
|
);
|
|
const streamingThreshold = useSelector(
|
|
state =>
|
|
state.dashboardInfo.common.conf.CSV_STREAMING_ROW_THRESHOLD ||
|
|
DEFAULT_CSV_STREAMING_ROW_THRESHOLD,
|
|
);
|
|
const datasource = useSelector(
|
|
state =>
|
|
(chart &&
|
|
chart.form_data &&
|
|
state.datasources[chart.form_data.datasource]) ||
|
|
PLACEHOLDER_DATASOURCE,
|
|
);
|
|
const dashboardInfo = useSelector(state => state.dashboardInfo);
|
|
|
|
const isCached = useMemo(
|
|
// eslint-disable-next-line camelcase
|
|
() => queriesResponse?.map(({ is_cached }) => is_cached) || [],
|
|
[queriesResponse],
|
|
);
|
|
|
|
const [descriptionHeight, setDescriptionHeight] = useState(0);
|
|
const [height, setHeight] = useState(props.height);
|
|
const [width, setWidth] = useState(props.width);
|
|
|
|
const [isStreamingModalVisible, setIsStreamingModalVisible] = useState(false);
|
|
const {
|
|
progress,
|
|
isExporting,
|
|
startExport,
|
|
cancelExport,
|
|
resetExport,
|
|
retryExport,
|
|
} = useStreamingExport({
|
|
onComplete: () => {
|
|
// Don't show toast here - wait for user to click Download button
|
|
},
|
|
onError: () => {
|
|
boundActionCreators.addDangerToast(t('Export failed - please try again'));
|
|
},
|
|
});
|
|
|
|
const handleDownloadComplete = useCallback(() => {
|
|
boundActionCreators.addSuccessToast(t('CSV file downloaded successfully'));
|
|
}, [boundActionCreators]);
|
|
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],
|
|
);
|
|
|
|
// Chart state handler for stateful charts
|
|
const handleChartStateChange = useCallback(
|
|
chartState => {
|
|
if (hasChartStateConverter(slice?.viz_type)) {
|
|
dispatch(updateChartState(props.id, slice.viz_type, chartState));
|
|
}
|
|
},
|
|
[dispatch, props.id, slice?.viz_type],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isExpanded) {
|
|
const descriptionHeight =
|
|
isExpanded && descriptionRef.current
|
|
? descriptionRef.current?.offsetHeight
|
|
: 0;
|
|
setDescriptionHeight(descriptionHeight);
|
|
} else {
|
|
setDescriptionHeight(0);
|
|
}
|
|
}, [isExpanded]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
resize.cancel();
|
|
},
|
|
[resize],
|
|
);
|
|
|
|
useEffect(() => {
|
|
resize();
|
|
}, [resize, props.isFullSize]);
|
|
|
|
const getHeaderHeight = useCallback(() => {
|
|
if (headerRef.current) {
|
|
const computedMarginBottom = getComputedStyle(
|
|
headerRef.current,
|
|
).getPropertyValue('margin-bottom');
|
|
const marginBottom = parseInt(computedMarginBottom, 10) || 0;
|
|
const computedHeight = getComputedStyle(
|
|
headerRef.current,
|
|
).getPropertyValue('height');
|
|
const height = parseInt(computedHeight, 10) || DEFAULT_HEADER_HEIGHT;
|
|
return height + marginBottom;
|
|
}
|
|
return DEFAULT_HEADER_HEIGHT;
|
|
}, [headerRef]);
|
|
|
|
const getChartHeight = useCallback(() => {
|
|
const headerHeight = getHeaderHeight();
|
|
return Math.max(height - headerHeight - descriptionHeight, 20);
|
|
}, [getHeaderHeight, height, descriptionHeight]);
|
|
|
|
const handleFilterMenuOpen = useCallback(
|
|
(chartId, column) => {
|
|
boundActionCreators.setFocusedFilterField(chartId, column);
|
|
},
|
|
[boundActionCreators.setFocusedFilterField],
|
|
);
|
|
|
|
const handleFilterMenuClose = useCallback(
|
|
(chartId, column) => {
|
|
boundActionCreators.unsetFocusedFilterField(chartId, column);
|
|
},
|
|
[boundActionCreators.unsetFocusedFilterField],
|
|
);
|
|
|
|
const logExploreChart = useCallback(() => {
|
|
boundActionCreators.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, {
|
|
slice_id: slice.slice_id,
|
|
is_cached: isCached,
|
|
});
|
|
}, [boundActionCreators.logEvent, slice.slice_id, isCached]);
|
|
|
|
const chartConfiguration = useSelector(
|
|
state => state.dashboardInfo.metadata?.chart_configuration,
|
|
);
|
|
const chartCustomizationItems = useSelector(
|
|
state =>
|
|
state.dashboardInfo.metadata?.chart_customization_config || EMPTY_ARRAY,
|
|
);
|
|
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 chartState = useSelector(
|
|
state => state.dashboardState.chartStates?.[props.id],
|
|
);
|
|
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,
|
|
),
|
|
);
|
|
const whatIfModifications = useSelector(
|
|
state => state.dashboardState.whatIfModifications || EMPTY_ARRAY,
|
|
);
|
|
|
|
const formData = useMemo(
|
|
() =>
|
|
getFormDataWithExtraFilters({
|
|
chart: { id: chart.id, form_data: chart.form_data }, // avoid passing the whole chart object
|
|
chartConfiguration,
|
|
chartCustomizationItems,
|
|
filters: getAppliedFilterValues(props.id),
|
|
colorScheme,
|
|
colorNamespace,
|
|
sliceId: props.id,
|
|
nativeFilters,
|
|
allSliceIds,
|
|
dataMask,
|
|
extraControls: props.extraControls,
|
|
labelsColor,
|
|
labelsColorMap,
|
|
sharedLabelsColors,
|
|
ownColorScheme,
|
|
whatIfModifications,
|
|
}),
|
|
[
|
|
chart.id,
|
|
chart.form_data,
|
|
chartConfiguration,
|
|
chartCustomizationItems,
|
|
props.id,
|
|
props.extraControls,
|
|
colorScheme,
|
|
colorNamespace,
|
|
nativeFilters,
|
|
allSliceIds,
|
|
dataMask,
|
|
labelsColor,
|
|
labelsColorMap,
|
|
sharedLabelsColors,
|
|
ownColorScheme,
|
|
whatIfModifications,
|
|
],
|
|
);
|
|
|
|
formData.dashboardId = dashboardInfo.id;
|
|
|
|
const ownState = useMemo(() => {
|
|
const baseOwnState = dataMask[props.id]?.ownState || EMPTY_OBJECT;
|
|
// Create a chartState-like object that includes state from chartState or formData fallbacks
|
|
const chartStateForConversion = {
|
|
state: getChartStateWithFallback(chartState, formData, slice.viz_type),
|
|
};
|
|
return createOwnStateWithChartState(
|
|
baseOwnState,
|
|
chartStateForConversion,
|
|
slice.viz_type,
|
|
);
|
|
}, [
|
|
dataMask[props.id]?.ownState,
|
|
props.id,
|
|
slice.viz_type,
|
|
chartState?.state,
|
|
formData,
|
|
]);
|
|
|
|
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 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: isCached,
|
|
});
|
|
|
|
const exportFormData = isFullCSV
|
|
? { ...formData, row_limit: maxRows }
|
|
: formData;
|
|
const resultType = isPivot ? 'post_processed' : 'full';
|
|
|
|
let actualRowCount;
|
|
const isTableViz = formData?.viz_type === 'table';
|
|
|
|
if (
|
|
isTableViz &&
|
|
queriesResponse?.length > 1 &&
|
|
queriesResponse[1]?.data?.[0]?.rowcount
|
|
) {
|
|
actualRowCount = queriesResponse[1].data[0].rowcount;
|
|
} else if (queriesResponse?.[0]?.sql_rowcount != null) {
|
|
actualRowCount = queriesResponse[0].sql_rowcount;
|
|
} else {
|
|
actualRowCount = exportFormData?.row_limit;
|
|
}
|
|
|
|
// Handle streaming CSV exports based on row threshold
|
|
const shouldUseStreaming =
|
|
format === 'csv' && !isPivot && actualRowCount >= streamingThreshold;
|
|
let filename;
|
|
if (shouldUseStreaming) {
|
|
const now = new Date();
|
|
const date = now.toISOString().slice(0, 10);
|
|
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
const timestamp = `_${date}_${time}`;
|
|
const chartName = slice.slice_name || formData.viz_type || 'chart';
|
|
const safeChartName = chartName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
filename = `${safeChartName}${timestamp}.csv`;
|
|
}
|
|
const baseOwnState = dataMask[props.id]?.ownState || {};
|
|
const state = getChartStateWithFallback(
|
|
chartState,
|
|
formData,
|
|
slice.viz_type,
|
|
);
|
|
|
|
const ownState = state
|
|
? {
|
|
...baseOwnState,
|
|
...convertChartStateToOwnState(slice.viz_type, state),
|
|
}
|
|
: baseOwnState;
|
|
|
|
exportChart({
|
|
formData: exportFormData,
|
|
resultType,
|
|
resultFormat: format,
|
|
force: true,
|
|
ownState,
|
|
onStartStreamingExport: shouldUseStreaming
|
|
? exportParams => {
|
|
setIsStreamingModalVisible(true);
|
|
startExport({
|
|
...exportParams,
|
|
filename,
|
|
expectedRows: actualRowCount,
|
|
});
|
|
}
|
|
: null,
|
|
});
|
|
},
|
|
[
|
|
slice.slice_id,
|
|
slice.viz_type,
|
|
isCached,
|
|
formData,
|
|
maxRows,
|
|
dataMask[props.id]?.ownState,
|
|
chartState,
|
|
props.id,
|
|
boundActionCreators.logEvent,
|
|
queriesResponse,
|
|
startExport,
|
|
resetExport,
|
|
streamingThreshold,
|
|
],
|
|
);
|
|
|
|
const exportCSV = useCallback(() => {
|
|
exportTable('csv', false);
|
|
}, [exportTable]);
|
|
|
|
const exportFullCSV = useCallback(() => {
|
|
exportTable('csv', true);
|
|
}, [exportTable]);
|
|
|
|
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: isCached,
|
|
});
|
|
return boundActionCreators.refreshChart(chart.id, true, props.dashboardId);
|
|
}, [
|
|
boundActionCreators.refreshChart,
|
|
chart.id,
|
|
props.dashboardId,
|
|
slice.slice_id,
|
|
isCached,
|
|
boundActionCreators.logEvent,
|
|
]);
|
|
|
|
if (chart === EMPTY_OBJECT || slice === EMPTY_OBJECT) {
|
|
return <MissingChart height={getChartHeight()} />;
|
|
}
|
|
|
|
const isLoading = chartStatus === 'loading';
|
|
const cachedDttm =
|
|
// eslint-disable-next-line camelcase
|
|
queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
|
|
|
|
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()}
|
|
exportPivotExcel={exportPivotExcel}
|
|
/>
|
|
|
|
{/*
|
|
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={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(),
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<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={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}
|
|
onChartStateChange={handleChartStateChange}
|
|
/>
|
|
</ChartWrapper>
|
|
|
|
<StreamingExportModal
|
|
visible={isStreamingModalVisible}
|
|
onCancel={() => {
|
|
cancelExport();
|
|
setIsStreamingModalVisible(false);
|
|
resetExport();
|
|
}}
|
|
onRetry={retryExport}
|
|
onDownload={handleDownloadComplete}
|
|
progress={progress}
|
|
exportType="csv"
|
|
/>
|
|
</SliceContainer>
|
|
);
|
|
};
|
|
|
|
Chart.propTypes = propTypes;
|
|
|
|
export default memo(Chart, (prevProps, nextProps) => {
|
|
if (prevProps.cacheBusterProp !== nextProps.cacheBusterProp) {
|
|
return false;
|
|
}
|
|
return (
|
|
!nextProps.isComponentVisible ||
|
|
(prevProps.componentId === nextProps.componentId &&
|
|
prevProps.isComponentVisible &&
|
|
prevProps.isInView === nextProps.isInView &&
|
|
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)
|
|
);
|
|
});
|