mirror of
https://github.com/apache/superset.git
synced 2026-07-25 16:12:39 +00:00
chore(lint): convert class components to function components
Convert all remaining React class components to function components using hooks (useState, useCallback, useEffect, useRef, useMemo) to satisfy the react-prefer-function-component ESLint rule. Key changes: - Converted components in dashboard, explore, SqlLab, and Chart areas - Updated associated test files with proper typing - Fixed JSX.Element return types for components used as JSX - Added explicit ControlHeader props where needed - Fixed shouldFocus callback signature in WithPopoverMenu usage Notable exceptions (not converted): - ErrorBoundary (uses componentDidCatch) - DragDroppable (react-dnd requires class instances) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { connect } from 'react-redux';
|
||||
import { PureComponent } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { t } from '@apache-superset/core';
|
||||
import {
|
||||
HandlerFunction,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
Payload,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { SupersetTheme, withTheme } from '@apache-superset/core/ui';
|
||||
import { SupersetTheme, useTheme } from '@apache-superset/core/ui';
|
||||
import {
|
||||
AsyncEsmComponent,
|
||||
List,
|
||||
@@ -72,7 +72,7 @@ export interface Props {
|
||||
value: Annotation[];
|
||||
onChange: (annotations: Annotation[]) => void;
|
||||
refreshAnnotationData: (payload: Payload) => void;
|
||||
theme: SupersetTheme;
|
||||
theme?: SupersetTheme;
|
||||
}
|
||||
|
||||
export interface PopoverState {
|
||||
@@ -80,200 +80,200 @@ export interface PopoverState {
|
||||
addedAnnotationIndex: number | null;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
vizType: '',
|
||||
value: [],
|
||||
annotationError: {},
|
||||
annotationQuery: {},
|
||||
onChange: () => {},
|
||||
};
|
||||
class AnnotationLayerControl extends PureComponent<Props, PopoverState> {
|
||||
static defaultProps = defaultProps;
|
||||
function AnnotationLayerControl({
|
||||
colorScheme,
|
||||
annotationError = {},
|
||||
annotationQuery = {},
|
||||
vizType = '',
|
||||
validationErrors,
|
||||
name,
|
||||
actions,
|
||||
value = [],
|
||||
onChange = () => {},
|
||||
refreshAnnotationData,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const [popoverVisible, setPopoverVisible] = useState<
|
||||
Record<number | string, boolean>
|
||||
>({});
|
||||
const [addedAnnotationIndex, setAddedAnnotationIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
constructor(props: 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);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// preload the AnnotationLayer component and dependent libraries i.e. mathjs
|
||||
// componentDidMount - preload the AnnotationLayer component and dependent libraries i.e. mathjs
|
||||
useEffect(() => {
|
||||
AnnotationLayer.preload();
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { name, annotationError, validationErrors, value } = this.props;
|
||||
// componentDidUpdate - sync validation errors
|
||||
useEffect(() => {
|
||||
if (
|
||||
(Object.keys(annotationError).length && !validationErrors.length) ||
|
||||
(!Object.keys(annotationError).length && validationErrors.length)
|
||||
) {
|
||||
if (
|
||||
annotationError !== prevProps.annotationError ||
|
||||
validationErrors !== prevProps.validationErrors ||
|
||||
value !== prevProps.value
|
||||
) {
|
||||
this.props.actions.setControlValue(
|
||||
name,
|
||||
value,
|
||||
Object.keys(annotationError),
|
||||
actions.setControlValue(name, value, Object.keys(annotationError));
|
||||
}
|
||||
}, [annotationError, validationErrors, value, actions, name]);
|
||||
|
||||
const addAnnotationLayer = useCallback(
|
||||
(originalAnnotation: Annotation | null, newAnnotation: Annotation) => {
|
||||
let annotations = value;
|
||||
if (originalAnnotation && annotations.includes(originalAnnotation)) {
|
||||
annotations = annotations.map(anno =>
|
||||
anno === originalAnnotation ? newAnnotation : anno,
|
||||
);
|
||||
} else {
|
||||
annotations = [...annotations, newAnnotation];
|
||||
setAddedAnnotationIndex(annotations.length - 1);
|
||||
}
|
||||
|
||||
refreshAnnotationData({
|
||||
annotation: newAnnotation,
|
||||
force: true,
|
||||
});
|
||||
|
||||
onChange(annotations);
|
||||
},
|
||||
[value, refreshAnnotationData, onChange],
|
||||
);
|
||||
|
||||
const handleVisibleChange = useCallback(
|
||||
(visible: boolean, popoverKey: number | string) => {
|
||||
setPopoverVisible(prev => ({
|
||||
...prev,
|
||||
[popoverKey]: visible,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const removeAnnotationLayer = useCallback(
|
||||
(annotation: Annotation | null) => {
|
||||
const annotations = value.filter(anno => anno !== annotation);
|
||||
// So scrollbar doesnt get stuck on hidden
|
||||
const element = getSectionContainerElement();
|
||||
if (element) {
|
||||
element.style.setProperty('overflow-y', 'auto', 'important');
|
||||
}
|
||||
onChange(annotations);
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
const renderPopover = useCallback(
|
||||
(
|
||||
popoverKey: number | string,
|
||||
annotation: Annotation | null,
|
||||
error: string,
|
||||
) => {
|
||||
const id = annotation?.name || '_new';
|
||||
|
||||
return (
|
||||
<div id={`annotation-pop-${id}`} data-test="popover-content">
|
||||
<AnnotationLayer
|
||||
{...(annotation || {})}
|
||||
error={error}
|
||||
colorScheme={colorScheme}
|
||||
vizType={vizType}
|
||||
addAnnotationLayer={(newAnnotation: Annotation) =>
|
||||
addAnnotationLayer(annotation, newAnnotation)
|
||||
}
|
||||
removeAnnotationLayer={() => removeAnnotationLayer(annotation)}
|
||||
close={() => {
|
||||
handleVisibleChange(false, popoverKey);
|
||||
setAddedAnnotationIndex(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[
|
||||
colorScheme,
|
||||
vizType,
|
||||
addAnnotationLayer,
|
||||
removeAnnotationLayer,
|
||||
handleVisibleChange,
|
||||
],
|
||||
);
|
||||
|
||||
const renderInfo = useCallback(
|
||||
(anno: Annotation) => {
|
||||
if (annotationQuery[anno.name]) {
|
||||
return (
|
||||
<Icons.SyncOutlined iconColor={theme.colorPrimary} iconSize="m" />
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (annotationError[anno.name]) {
|
||||
return (
|
||||
<InfoTooltip
|
||||
label="validation-errors"
|
||||
type="error"
|
||||
tooltip={annotationError[anno.name]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!anno.show) {
|
||||
return <span style={{ color: theme.colorError }}> {t('Hidden')} </span>;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[annotationQuery, annotationError, theme],
|
||||
);
|
||||
|
||||
addAnnotationLayer = (
|
||||
originalAnnotation: Annotation | null,
|
||||
newAnnotation: Annotation,
|
||||
) => {
|
||||
let annotations = this.props.value;
|
||||
if (originalAnnotation && annotations.includes(originalAnnotation)) {
|
||||
annotations = annotations.map(anno =>
|
||||
anno === originalAnnotation ? newAnnotation : anno,
|
||||
);
|
||||
} else {
|
||||
annotations = [...annotations, newAnnotation];
|
||||
this.setState({ addedAnnotationIndex: annotations.length - 1 });
|
||||
}
|
||||
const addedAnnotation = useMemo(
|
||||
() => (addedAnnotationIndex !== null ? value[addedAnnotationIndex] : null),
|
||||
[addedAnnotationIndex, value],
|
||||
);
|
||||
|
||||
this.props.refreshAnnotationData({
|
||||
annotation: newAnnotation,
|
||||
force: true,
|
||||
});
|
||||
const annotations = value.map((anno, i) => (
|
||||
<ControlPopover
|
||||
key={i}
|
||||
trigger="click"
|
||||
title={t('Edit annotation layer')}
|
||||
css={thm => ({
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: thm.colorFillContentHover,
|
||||
},
|
||||
})}
|
||||
content={renderPopover(i, anno, annotationError[anno.name])}
|
||||
open={popoverVisible[i]}
|
||||
onOpenChange={visible => handleVisibleChange(visible, i)}
|
||||
>
|
||||
<CustomListItem selectable>
|
||||
<span>{anno.name}</span>
|
||||
<span style={{ float: 'right' }}>{renderInfo(anno)}</span>
|
||||
</CustomListItem>
|
||||
</ControlPopover>
|
||||
));
|
||||
|
||||
this.props.onChange(annotations);
|
||||
};
|
||||
const addLayerPopoverKey = 'add';
|
||||
|
||||
handleVisibleChange = (visible: boolean, popoverKey: number | string) => {
|
||||
this.setState(prevState => ({
|
||||
popoverVisible: { ...prevState.popoverVisible, [popoverKey]: visible },
|
||||
}));
|
||||
};
|
||||
|
||||
removeAnnotationLayer(annotation: Annotation | null) {
|
||||
const annotations = this.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: number | string,
|
||||
annotation: Annotation | null,
|
||||
error: string,
|
||||
) => {
|
||||
const id = annotation?.name || '_new';
|
||||
|
||||
return (
|
||||
<div id={`annotation-pop-${id}`} data-test="popover-content">
|
||||
<AnnotationLayer
|
||||
{...(annotation || {})}
|
||||
error={error}
|
||||
colorScheme={this.props.colorScheme}
|
||||
vizType={this.props.vizType}
|
||||
addAnnotationLayer={(newAnnotation: Annotation) =>
|
||||
this.addAnnotationLayer(annotation, newAnnotation)
|
||||
return (
|
||||
<div>
|
||||
<List bordered css={thm => ({ borderRadius: thm.borderRadius })}>
|
||||
{annotations}
|
||||
<ControlPopover
|
||||
trigger="click"
|
||||
content={renderPopover(addLayerPopoverKey, addedAnnotation, '')}
|
||||
title={t('Add annotation layer')}
|
||||
open={popoverVisible[addLayerPopoverKey]}
|
||||
destroyTooltipOnHide
|
||||
onOpenChange={visible =>
|
||||
handleVisibleChange(visible, addLayerPopoverKey)
|
||||
}
|
||||
removeAnnotationLayer={() => this.removeAnnotationLayer(annotation)}
|
||||
close={() => {
|
||||
this.handleVisibleChange(false, popoverKey);
|
||||
this.setState({ addedAnnotationIndex: null });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderInfo(anno: Annotation) {
|
||||
const { annotationError, annotationQuery, theme } = this.props;
|
||||
if (annotationQuery[anno.name]) {
|
||||
return <Icons.SyncOutlined iconColor={theme.colorPrimary} iconSize="m" />;
|
||||
}
|
||||
if (annotationError[anno.name]) {
|
||||
return (
|
||||
<InfoTooltip
|
||||
label="validation-errors"
|
||||
type="error"
|
||||
tooltip={annotationError[anno.name]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!anno.show) {
|
||||
return <span style={{ color: theme.colorError }}> {t('Hidden')} </span>;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const { addedAnnotationIndex } = this.state;
|
||||
const addedAnnotation =
|
||||
addedAnnotationIndex !== null
|
||||
? this.props.value[addedAnnotationIndex]
|
||||
: null;
|
||||
const annotations = this.props.value.map((anno, i) => (
|
||||
<ControlPopover
|
||||
key={i}
|
||||
trigger="click"
|
||||
title={t('Edit annotation layer')}
|
||||
css={theme => ({
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.colorFillContentHover,
|
||||
},
|
||||
})}
|
||||
content={this.renderPopover(
|
||||
i,
|
||||
anno,
|
||||
this.props.annotationError[anno.name],
|
||||
)}
|
||||
open={this.state.popoverVisible[i]}
|
||||
onOpenChange={visible => this.handleVisibleChange(visible, i)}
|
||||
>
|
||||
<CustomListItem selectable>
|
||||
<span>{anno.name}</span>
|
||||
<span style={{ float: 'right' }}>{this.renderInfo(anno)}</span>
|
||||
</CustomListItem>
|
||||
</ControlPopover>
|
||||
));
|
||||
const addLayerPopoverKey = 'add';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<List bordered css={theme => ({ borderRadius: theme.borderRadius })}>
|
||||
{annotations}
|
||||
<ControlPopover
|
||||
trigger="click"
|
||||
content={this.renderPopover(
|
||||
addLayerPopoverKey,
|
||||
addedAnnotation,
|
||||
'',
|
||||
)}
|
||||
title={t('Add annotation layer')}
|
||||
open={this.state.popoverVisible[addLayerPopoverKey]}
|
||||
destroyTooltipOnHide
|
||||
onOpenChange={visible =>
|
||||
this.handleVisibleChange(visible, addLayerPopoverKey)
|
||||
}
|
||||
>
|
||||
<CustomListItem selectable>
|
||||
<Icons.PlusOutlined
|
||||
iconSize="m"
|
||||
data-test="add-annotation-layer-button"
|
||||
/>
|
||||
{t('Add annotation layer')}
|
||||
</CustomListItem>
|
||||
</ControlPopover>
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
>
|
||||
<CustomListItem selectable>
|
||||
<Icons.PlusOutlined
|
||||
iconSize="m"
|
||||
data-test="add-annotation-layer-button"
|
||||
/>
|
||||
{t('Add annotation layer')}
|
||||
</CustomListItem>
|
||||
</ControlPopover>
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Tried to hook this up through stores/control.jsx instead of using redux
|
||||
@@ -316,9 +316,7 @@ function mapDispatchToProps(
|
||||
};
|
||||
}
|
||||
|
||||
const themedAnnotationLayerControl = withTheme(AnnotationLayerControl);
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(themedAnnotationLayerControl);
|
||||
)(AnnotationLayerControl);
|
||||
|
||||
Reference in New Issue
Block a user