diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx index 563b94682385e..12683adef94a9 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import rison from 'rison'; import PropTypes from 'prop-types'; import { CompactPicker } from 'react-color'; @@ -131,128 +131,51 @@ const NotFoundContent = () => ( ); -class AnnotationLayer extends React.PureComponent { - constructor(props) { - super(props); - const { - name, - annotationType, - sourceType, - color, - opacity, - style, - width, - showMarkers, - hideLine, - value, - overrides, - show, - showLabel, - titleColumn, - descriptionColumns, - timeColumn, - intervalEndColumn, - vizType, - } = props; - - // Only allow override whole time_range - if ('since' in overrides || 'until' in overrides) { - overrides.time_range = null; - delete overrides.since; - delete overrides.until; - } - - // Check if annotationType is supported by this chart - const metadata = getChartMetadataRegistry().get(vizType); - const supportedAnnotationTypes = metadata?.supportedAnnotationTypes || []; - const validAnnotationType = supportedAnnotationTypes.includes( - annotationType, - ) - ? annotationType - : supportedAnnotationTypes[0]; - - this.state = { - // base - name, - annotationType: validAnnotationType, - sourceType, - value, - overrides, - show, - showLabel, - // slice - titleColumn, - descriptionColumns, - timeColumn, - intervalEndColumn, - // display - color: color || AUTOMATIC_COLOR, - opacity, - style, - width, - showMarkers, - hideLine, - // refData - isNew: !name, - isLoadingOptions: true, - valueOptions: [], - }; - this.submitAnnotation = this.submitAnnotation.bind(this); - this.deleteAnnotation = this.deleteAnnotation.bind(this); - this.applyAnnotation = this.applyAnnotation.bind(this); - this.fetchOptions = this.fetchOptions.bind(this); - this.handleAnnotationType = this.handleAnnotationType.bind(this); - this.handleAnnotationSourceType = - this.handleAnnotationSourceType.bind(this); - this.handleValue = this.handleValue.bind(this); - this.isValidForm = this.isValidForm.bind(this); - } - - componentDidMount() { - const { annotationType, sourceType, isLoadingOptions } = this.state; - this.fetchOptions(annotationType, sourceType, isLoadingOptions); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.sourceType !== this.state.sourceType) { - this.fetchOptions(this.state.annotationType, this.state.sourceType, true); - } - } - - getSupportedSourceTypes(annotationType) { - // Get vis types that can be source. - const sources = getChartMetadataRegistry() - .entries() - .filter(({ value: chartMetadata }) => - chartMetadata.canBeAnnotationType(annotationType), - ) - .map(({ key, value: chartMetadata }) => ({ - value: key, - label: chartMetadata.name, - })); - // Prepend native source if applicable - if (ANNOTATION_TYPES_METADATA[annotationType]?.supportNativeSource) { - sources.unshift(ANNOTATION_SOURCE_TYPES_METADATA.NATIVE); - } - return sources; - } - - isValidFormulaAnnotation(expression, annotationType) { - if (annotationType === ANNOTATION_TYPES.FORMULA) { - return isValidExpression(expression); +const AnnotationLayer = props => { + const [annotationType, setAnnotationType] = useState(validAnnotationType); + const [color, setColor] = useState(props.color || AUTOMATIC_COLOR); + const [isNew, setIsNew] = useState(!name); + const [isLoadingOptions, setIsLoadingOptions] = useState(true); + const [valueOptions, setValueOptions] = useState([]); + + useEffect(() => { + fetchOptions(annotationType, sourceType, isLoadingOptions); + }, [annotationType, isLoadingOptions]); + useEffect(() => { + if (prevState.sourceType !== sourceType) { + fetchOptions(annotationType, sourceType, true); } - return true; - } - - isValidForm() { - const { - name, - annotationType, - sourceType, - value, - timeColumn, - intervalEndColumn, - } = this.state; + }, [annotationType]); + const getSupportedSourceTypesHandler = useCallback( + annotationType => { + // Get vis types that can be source. + const sources = getChartMetadataRegistry() + .entries() + .filter(({ value: chartMetadata }) => + chartMetadata.canBeAnnotationType(annotationType), + ) + .map(({ key, value: chartMetadata }) => ({ + value: key, + label: chartMetadata.name, + })); + // Prepend native source if applicable + if (ANNOTATION_TYPES_METADATA[annotationType]?.supportNativeSource) { + sources.unshift(ANNOTATION_SOURCE_TYPES_METADATA.NATIVE); + } + return sources; + }, + [annotationType], + ); + const isValidFormulaAnnotationHandler = useCallback( + (expression, annotationType) => { + if (annotationType === ANNOTATION_TYPES.FORMULA) { + return isValidExpression(expression); + } + return true; + }, + [annotationType], + ); + const isValidFormHandler = useCallback(() => { const errors = [ validateNonEmpty(name), validateNonEmpty(annotationType), @@ -267,38 +190,33 @@ class AnnotationLayer extends React.PureComponent { errors.push(validateNonEmpty(intervalEndColumn)); } } - errors.push(!this.isValidFormulaAnnotation(value, annotationType)); + errors.push(!isValidFormulaAnnotationHandler(value, annotationType)); return !errors.filter(x => x).length; - } - - handleAnnotationType(annotationType) { - this.setState({ - annotationType, - sourceType: null, - value: null, - }); - } - - handleAnnotationSourceType(sourceType) { - const { sourceType: prevSourceType } = this.state; - + }, [annotationType]); + const handleAnnotationTypeHandler = useCallback( + annotationType => { + setAnnotationType(annotationType); + setSourceType(null); + setValue(null); + }, + [annotationType], + ); + const handleAnnotationSourceTypeHandler = useCallback(sourceType => { if (prevSourceType !== sourceType) { - this.setState({ sourceType, value: null, isLoadingOptions: true }); + setSourceType(sourceType); + setValue(null); + setIsLoadingOptions(true); } - } - - handleValue(value) { - this.setState({ - value, - descriptionColumns: [], - intervalEndColumn: null, - timeColumn: null, - titleColumn: null, - overrides: { time_range: null }, - }); - } - - fetchOptions(annotationType, sourceType, isLoadingOptions) { + }, []); + const handleValueHandler = useCallback(value => { + setValue(value); + setDescriptionColumns([]); + setIntervalEndColumn(null); + setTimeColumn(null); + setTitleColumn(null); + setOverrides({ time_range: null }); + }, []); + const fetchOptions = useMemo(() => { if (isLoadingOptions) { if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { const queryParams = rison.encode({ @@ -314,10 +232,8 @@ class AnnotationLayer extends React.PureComponent { label: layer.name, })) : []; - this.setState({ - isLoadingOptions: false, - valueOptions: layers, - }); + setIsLoadingOptions(false); + setValueOptions(layers); }); } else if (requiresQuery(sourceType)) { const queryParams = rison.encode({ @@ -337,9 +253,9 @@ class AnnotationLayer extends React.PureComponent { endpoint: `/api/v1/chart/?q=${queryParams}`, }).then(({ json }) => { const registry = getChartMetadataRegistry(); - this.setState({ - isLoadingOptions: false, - valueOptions: json.result + setIsLoadingOptions(false); + setValueOptions( + json.result .filter(x => { const metadata = registry.get(x.viz_type); return metadata && metadata.canBeAnnotationType(annotationType); @@ -357,24 +273,20 @@ class AnnotationLayer extends React.PureComponent { }, }, })), - }); + ); }); } else { - this.setState({ - isLoadingOptions: false, - valueOptions: [], - }); + setIsLoadingOptions(false); + setValueOptions([]); } } - } - - deleteAnnotation() { - this.props.removeAnnotationLayer(); - this.props.close(); - } - - applyAnnotation() { - if (this.isValidForm()) { + }, [isLoadingOptions, annotationType]); + const deleteAnnotationHandler = useCallback(() => { + props.removeAnnotationLayer(); + props.close(); + }, []); + const applyAnnotationHandler = useCallback(() => { + if (isValidFormHandler()) { const annotationFields = [ 'name', 'annotationType', @@ -396,8 +308,8 @@ class AnnotationLayer extends React.PureComponent { ]; const newAnnotation = {}; annotationFields.forEach(field => { - if (this.state[field] !== null) { - newAnnotation[field] = this.state[field]; + if (stateHandler[field] !== null) { + newAnnotation[field] = stateHandler[field]; } }); @@ -405,17 +317,15 @@ class AnnotationLayer extends React.PureComponent { newAnnotation.color = null; } - this.props.addAnnotationLayer(newAnnotation); - this.setState({ isNew: false }); + props.addAnnotationLayer(newAnnotation); + setIsNew(false); } - } - - submitAnnotation() { - this.applyAnnotation(); - this.props.close(); - } - - renderOption(option) { + }, []); + const submitAnnotationHandler = useCallback(() => { + applyAnnotationHandler(); + props.close(); + }, []); + const renderOptionHandler = useCallback(option => { return ( ); - } - - renderValueConfiguration() { - const { - annotationType, - sourceType, - value, - valueOptions, - isLoadingOptions, - } = this.state; + }, []); + const renderValueConfigurationHandler = useCallback(() => { let label = ''; let description = ''; if (requiresQuery(sourceType)) { @@ -449,7 +351,7 @@ class AnnotationLayer extends React.PureComponent { description = t( `Use another existing chart as a source for annotations and overlays. Your chart must be one of these visualization types: [%s]`, - this.getSupportedSourceTypes(annotationType) + getSupportedSourceTypesHandler(annotationType) .map(x => x.label) .join(', '), ); @@ -473,9 +375,9 @@ class AnnotationLayer extends React.PureComponent { options={valueOptions} isLoading={isLoadingOptions} value={value} - onChange={this.handleValue} + onChange={handleValueHandler} validationErrors={!value ? ['Mandatory'] : []} - optionRenderer={this.renderOption} + optionRenderer={renderOptionHandler} notFoundContent={} /> ); @@ -490,9 +392,9 @@ class AnnotationLayer extends React.PureComponent { label={label} placeholder="" value={value} - onChange={this.handleValue} + onChange={handleValueHandler} validationErrors={ - !this.isValidFormulaAnnotation(value, annotationType) + !isValidFormulaAnnotationHandler(value, annotationType) ? [t('Bad formula.')] : [] } @@ -500,20 +402,8 @@ class AnnotationLayer extends React.PureComponent { ); } return ''; - } - - renderSliceConfiguration() { - const { - annotationType, - sourceType, - value, - valueOptions, - overrides, - titleColumn, - timeColumn, - intervalEndColumn, - descriptionColumns, - } = this.state; + }, [annotationType, valueOptions, isLoadingOptions]); + const renderSliceConfigurationHandler = useCallback(() => { const { slice } = valueOptions.find(x => x.value === value) || {}; if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) { const columns = (slice.data.groupby || []) @@ -548,7 +438,9 @@ class AnnotationLayer extends React.PureComponent { clearable={false} options={timeColumnOptions} value={timeColumn} - onChange={v => this.setState({ timeColumn: v })} + onChange={v => { + setTimeColumn(v); + }} /> )} {annotationType === ANNOTATION_TYPES.INTERVAL && ( @@ -563,7 +455,9 @@ class AnnotationLayer extends React.PureComponent { validationErrors={!intervalEndColumn ? ['Mandatory'] : []} options={columns} value={intervalEndColumn} - onChange={value => this.setState({ intervalEndColumn: value })} + onChange={value => { + setIntervalEndColumn(value); + }} /> )} this.setState({ titleColumn: value })} + onChange={value => { + setTitleColumn(value); + }} /> {annotationType !== ANNOTATION_TYPES.TIME_SERIES && ( this.setState({ descriptionColumns: value })} + onChange={value => { + setDescriptionColumns(value); + }} /> )}
@@ -602,11 +500,9 @@ class AnnotationLayer extends React.PureComponent { onChange={v => { delete overrides.time_range; if (v) { - this.setState({ - overrides: { ...overrides, time_range: null }, - }); + setOverrides({ ...overrides, time_range: null }); } else { - this.setState({ overrides: { ...overrides } }); + setOverrides({ ...overrides }); } }} /> @@ -621,15 +517,13 @@ class AnnotationLayer extends React.PureComponent { delete overrides.time_grain_sqla; delete overrides.granularity; if (v) { - this.setState({ - overrides: { - ...overrides, - time_grain_sqla: null, - granularity: null, - }, + setOverrides({ + ...overrides, + time_grain_sqla: null, + granularity: null, }); } else { - this.setState({ overrides: { ...overrides } }); + setOverrides({ ...overrides }); } }} /> @@ -641,9 +535,9 @@ class AnnotationLayer extends React.PureComponent { (example: 24 hours, 7 days, 56 weeks, 365 days)`)} placeholder="" value={overrides.time_shift} - onChange={v => - this.setState({ overrides: { ...overrides, time_shift: v } }) - } + onChange={v => { + setOverrides({ ...overrides, time_shift: v }); + }} />
@@ -651,20 +545,10 @@ class AnnotationLayer extends React.PureComponent { ); } return ''; - } - - renderDisplayConfiguration() { - const { - color, - opacity, - style, - width, - showMarkers, - hideLine, - annotationType, - } = this.state; + }, [valueOptions, annotationType]); + const renderDisplayConfiguration = useMemo(() => { const colorScheme = getCategoricalSchemeRegistry() - .get(this.props.colorScheme) + .get(props.colorScheme) .colors.concat(); if ( color && @@ -692,7 +576,9 @@ class AnnotationLayer extends React.PureComponent { ]} value={style} clearable={false} - onChange={v => this.setState({ style: v })} + onChange={v => { + setStyle(v); + }} /> this.setState({ opacity: value })} + onChange={value => { + setOpacity(value); + }} />
@@ -714,13 +602,17 @@ class AnnotationLayer extends React.PureComponent { this.setState({ color: v.hex })} + onChangeComplete={v => { + setColor(v.hex); + }} /> @@ -731,7 +623,9 @@ class AnnotationLayer extends React.PureComponent { label={t('Line width')} isInt value={width} - onChange={v => this.setState({ width: v })} + onChange={v => { + setWidth(v); + }} /> {annotationType === ANNOTATION_TYPES.TIME_SERIES && ( this.setState({ showMarkers: v })} + onChange={v => { + setShowMarkers(v); + }} /> )} {annotationType === ANNOTATION_TYPES.TIME_SERIES && ( @@ -750,125 +646,169 @@ class AnnotationLayer extends React.PureComponent { label={t('Hide Line')} description={t('Hides the Line for the time series')} value={hideLine} - onChange={v => this.setState({ hideLine: v })} + onChange={v => { + setHideLine(v); + }} /> )} ); - } - - render() { - const { isNew, name, annotationType, sourceType, show, showLabel } = - this.state; - const isValid = this.isValidForm(); - const metadata = getChartMetadataRegistry().get(this.props.vizType); - const supportedAnnotationTypes = metadata - ? metadata.supportedAnnotationTypes.map( - type => ANNOTATION_TYPES_METADATA[type], - ) - : []; - const supportedSourceTypes = this.getSupportedSourceTypes(annotationType); - - return ( - <> - {this.props.error && ( - - ERROR: {this.props.error} - - )} -
-
- - this.setState({ name: v })} - validationErrors={!name ? [t('Mandatory')] : []} - /> - this.setState({ show: !v })} - /> - this.setState({ showLabel: v })} - /> + }, [color, annotationType]); + + const { + name, + annotationType: annotationTypeProp, + sourceType, + opacity, + style, + width, + showMarkers, + hideLine, + value, + overrides, + show, + showLabel, + titleColumn, + descriptionColumns, + timeColumn, + intervalEndColumn, + vizType, + } = props; + overrides.time_range = null; + delete overrides.since; + delete overrides.until; + const metadata = getChartMetadataRegistry().get(vizType); + const supportedAnnotationTypes = metadata?.supportedAnnotationTypes || []; + const validAnnotationType = supportedAnnotationTypes.includes( + annotationTypeProp, + ) + ? annotationTypeProp + : supportedAnnotationTypes[0]; + + submitAnnotationHandler = submitAnnotationHandler.bind(this); + deleteAnnotationHandler = deleteAnnotationHandler.bind(this); + applyAnnotationHandler = applyAnnotationHandler.bind(this); + fetchOptions = fetchOptions.bind(this); + handleAnnotationTypeHandler = handleAnnotationTypeHandler.bind(this); + handleAnnotationSourceTypeHandler = + handleAnnotationSourceTypeHandler.bind(this); + handleValueHandler = handleValueHandler.bind(this); + isValidFormHandler = isValidFormHandler.bind(this); + + const isValid = isValidFormHandler(); + const metadata = getChartMetadataRegistry().get(props.vizType); + const supportedAnnotationTypes = metadata + ? metadata.supportedAnnotationTypes.map( + type => ANNOTATION_TYPES_METADATA[type], + ) + : []; + const supportedSourceTypes = getSupportedSourceTypesHandler(annotationType); + + return ( + <> + {props.error && ( + + ERROR: {props.error} + + )} +
+
+ + { + setName(v); + }} + validationErrors={!name ? [t('Mandatory')] : []} + /> + { + setShow(!v); + }} + /> + { + setShowLabel(v); + }} + /> + + {supportedSourceTypes.length > 0 && ( } + value={sourceType} + onChange={handleAnnotationSourceTypeHandler} + validationErrors={!sourceType ? [t('Mandatory')] : []} /> - {supportedSourceTypes.length > 0 && ( - } - value={sourceType} - onChange={this.handleAnnotationSourceType} - validationErrors={!sourceType ? [t('Mandatory')] : []} - /> - )} - {this.renderValueConfiguration()} - -
- {this.renderSliceConfiguration()} - {this.renderDisplayConfiguration()} + )} + {renderValueConfigurationHandler()} +
-
- {isNew ? ( - - ) : ( - - )} -
- - - -
+ {renderSliceConfigurationHandler()} + {renderDisplayConfiguration()} +
+
+ {isNew ? ( + + ) : ( + + )} +
+ + +
- - ); - } -} +
+ + ); +}; AnnotationLayer.propTypes = propTypes; AnnotationLayer.defaultProps = defaultProps; diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx index f70557170c0d8..b68aa5d586b9a 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx @@ -1,3 +1,4 @@ + /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -16,7 +17,7 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { List } from 'src/components'; import { connect } from 'react-redux'; @@ -27,7 +28,7 @@ import { getChartKey } from 'src/explore/exploreUtils'; import { runAnnotationQuery } from 'src/components/Chart/chartAction'; import CustomListItem from 'src/explore/components/controls/CustomListItem'; import ControlPopover, { - getSectionContainerElement, + getSectionContainerElement, } from '../ControlPopover/ControlPopover'; const AnnotationLayer = AsyncEsmComponent( @@ -58,73 +59,60 @@ const defaultProps = { onChange: () => {}, }; -class AnnotationLayerControl extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - popoverVisible: {}, - addedAnnotationIndex: null, - }; - this.addAnnotationLayer = this.addAnnotationLayer.bind(this); - this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this); - this.handleVisibleChange = this.handleVisibleChange.bind(this); - } +const AnnotationLayerControl = (props) => { + const [popoverVisible, setPopoverVisible] = useState({}); + const [addedAnnotationIndex, setAddedAnnotationIndex] = useState(null); - componentDidMount() { + useEffect(() => { // preload the AnnotationLayer component and dependent libraries i.e. mathjs AnnotationLayer.preload(); - } - - UNSAFE_componentWillReceiveProps(nextProps) { + }, []); + const UNSAFE_componentWillReceivePropsHandler = useCallback((nextProps) => { const { name, annotationError, validationErrors, value } = nextProps; if (Object.keys(annotationError).length && !validationErrors.length) { - this.props.actions.setControlValue( + props.actions.setControlValue( name, value, Object.keys(annotationError), ); } if (!Object.keys(annotationError).length && validationErrors.length) { - this.props.actions.setControlValue(name, value, []); + props.actions.setControlValue(name, value, []); } - } - - addAnnotationLayer(originalAnnotation, newAnnotation) { - let annotations = this.props.value; + }, []); + const addAnnotationLayerHandler = useCallback((originalAnnotation, newAnnotation) => { + let annotations = props.value; if (annotations.includes(originalAnnotation)) { annotations = annotations.map(anno => anno === originalAnnotation ? newAnnotation : anno, ); } else { annotations = [...annotations, newAnnotation]; - this.setState({ addedAnnotationIndex: annotations.length - 1 }); + setAddedAnnotationIndex(annotations.length - 1); } - this.props.refreshAnnotationData({ + props.refreshAnnotationData({ annotation: newAnnotation, force: true, }); - this.props.onChange(annotations); - } - - handleVisibleChange(visible, popoverKey) { - this.setState(prevState => ({ + props.onChange(annotations); + }, []); + const handleVisibleChangeHandler = useCallback((visible, popoverKey) => { + setStateHandler(prevState => ({ popoverVisible: { ...prevState.popoverVisible, [popoverKey]: visible }, })); - } - - removeAnnotationLayer(annotation) { - const annotations = this.props.value.filter(anno => anno !== annotation); + }, []); + const removeAnnotationLayerHandler = useCallback((annotation) => { + const annotations = props.value.filter(anno => anno !== annotation); // So scrollbar doesnt get stuck on hidden const element = getSectionContainerElement(); if (element) { element.style.setProperty('overflow-y', 'auto', 'important'); } - this.props.onChange(annotations); - } - - renderPopover(popoverKey, annotation, error) { + props.onChange(annotations); + }, []); + const renderPopoverHandler = useCallback((popoverKey, annotation, error) => { const id = annotation?.name || '_new'; return ( @@ -132,23 +120,22 @@ class AnnotationLayerControl extends React.PureComponent { - this.addAnnotationLayer(annotation, newAnnotation) + addAnnotationLayerHandler(annotation, newAnnotation) } - removeAnnotationLayer={() => this.removeAnnotationLayer(annotation)} + removeAnnotationLayer={() => removeAnnotationLayerHandler(annotation)} close={() => { - this.handleVisibleChange(false, popoverKey); - this.setState({ addedAnnotationIndex: null }); + handleVisibleChangeHandler(false, popoverKey); + setAddedAnnotationIndex(null); }} />
); - } - - renderInfo(anno) { - const { annotationError, annotationQuery, theme } = this.props; + }, []); + const renderInfoHandler = useCallback((anno) => { + const { annotationError, annotationQuery, theme } = props; if (annotationQuery[anno.name]) { return ( Hidden ; } return ''; - } + }, []); - render() { - const { addedAnnotationIndex } = this.state; - const addedAnnotation = this.props.value[addedAnnotationIndex]; + + addAnnotationLayerHandler = addAnnotationLayerHandler.bind(this); + removeAnnotationLayerHandler = removeAnnotationLayerHandler.bind(this); + handleVisibleChangeHandler = handleVisibleChangeHandler.bind(this); - const annotations = this.props.value.map((anno, i) => ( + const addedAnnotation = props.value[addedAnnotationIndex]; + + const annotations = props.value.map((anno, i) => ( this.handleVisibleChange(visible, i)} + visible={popoverVisible[i]} + onVisibleChange={visible => handleVisibleChangeHandler(visible, i)} > {anno.name} - {this.renderInfo(anno)} + {renderInfoHandler(anno)} )); @@ -210,12 +200,12 @@ class AnnotationLayerControl extends React.PureComponent { {annotations} - this.handleVisibleChange(visible, addLayerPopoverKey) + handleVisibleChangeHandler(visible, addLayerPopoverKey) } > @@ -228,9 +218,11 @@ class AnnotationLayerControl extends React.PureComponent {
- ); - } -} + ); +}; + + + AnnotationLayerControl.propTypes = propTypes; AnnotationLayerControl.defaultProps = defaultProps;