mirror of
https://github.com/apache/superset.git
synced 2026-04-22 17:45:21 +00:00
638 lines
19 KiB
JavaScript
638 lines
19 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 {
|
|
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 } 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,
|
|
};
|
|
|
|
// we use state + shouldComponentUpdate() logic to prevent perf-wrecking
|
|
// resizing across all slices on a dashboard on every update
|
|
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 = [];
|
|
|
|
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 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 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 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,
|
|
}),
|
|
[
|
|
chart.id,
|
|
chart.form_data,
|
|
chartConfiguration,
|
|
chartCustomizationItems,
|
|
props.id,
|
|
props.extraControls,
|
|
colorScheme,
|
|
colorNamespace,
|
|
nativeFilters,
|
|
allSliceIds,
|
|
dataMask,
|
|
labelsColor,
|
|
labelsColorMap,
|
|
sharedLabelsColors,
|
|
ownColorScheme,
|
|
],
|
|
);
|
|
|
|
formData.dashboardId = dashboardInfo.id;
|
|
|
|
const ownState = useMemo(() => {
|
|
const baseOwnState = dataMask[props.id]?.ownState || EMPTY_OBJECT;
|
|
|
|
if (hasChartStateConverter(slice.viz_type) && chartState?.state) {
|
|
return {
|
|
...baseOwnState,
|
|
...convertChartStateToOwnState(slice.viz_type, chartState.state),
|
|
chartState: chartState.state,
|
|
};
|
|
}
|
|
|
|
return baseOwnState;
|
|
}, [
|
|
dataMask[props.id]?.ownState,
|
|
props.id,
|
|
slice.viz_type,
|
|
chartState?.state,
|
|
]);
|
|
|
|
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,
|
|
});
|
|
|
|
let ownState = dataMask[props.id]?.ownState || {};
|
|
|
|
// Convert chart-specific state to backend format using registered converter
|
|
if (hasChartStateConverter(slice.viz_type) && chartState?.state) {
|
|
const convertedState = convertChartStateToOwnState(
|
|
slice.viz_type,
|
|
chartState.state,
|
|
);
|
|
ownState = {
|
|
...ownState,
|
|
...convertedState,
|
|
};
|
|
}
|
|
|
|
exportChart({
|
|
formData: isFullCSV ? { ...formData, row_limit: maxRows } : formData,
|
|
resultType: isPivot ? 'post_processed' : 'full',
|
|
resultFormat: format,
|
|
force: true,
|
|
ownState,
|
|
});
|
|
},
|
|
[
|
|
slice.slice_id,
|
|
slice.viz_type,
|
|
isCached,
|
|
formData,
|
|
maxRows,
|
|
dataMask[props.id]?.ownState,
|
|
chartState,
|
|
props.id,
|
|
boundActionCreators.logEvent,
|
|
],
|
|
);
|
|
|
|
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>
|
|
</SliceContainer>
|
|
);
|
|
};
|
|
|
|
Chart.propTypes = propTypes;
|
|
|
|
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)
|
|
);
|
|
});
|