/**
* 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 { connect } from 'react-redux';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { t } from '@apache-superset/core';
import {
HandlerFunction,
JsonObject,
Payload,
QueryFormData,
} from '@superset-ui/core';
import { SupersetTheme, useTheme } from '@apache-superset/core/ui';
import {
AsyncEsmComponent,
List,
InfoTooltip,
} from '@superset-ui/core/components';
import { getChartKey } from 'src/explore/exploreUtils';
import { runAnnotationQuery } from 'src/components/Chart/chartAction';
import CustomListItem from 'src/explore/components/controls/CustomListItem';
import { ChartState, ExplorePageState } from 'src/explore/types';
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { Icons } from '@superset-ui/core/components/Icons';
import ControlPopover, {
getSectionContainerElement,
} from '../ControlPopover/ControlPopover';
const AnnotationLayer = AsyncEsmComponent(
() => import('./AnnotationLayer'),
// size of overlay inner content
() =>
,
);
export interface Annotation {
name: string;
show?: boolean;
annotation: string;
timeout: Date;
key: string;
formData: QueryFormData | null;
isDashboardRequest?: boolean;
force?: boolean;
}
export interface Props {
colorScheme: string;
annotationError: Record;
annotationQuery: Record;
vizType: string;
validationErrors: JsonObject[];
name: string;
actions: {
setControlValue: HandlerFunction;
};
value: Annotation[];
onChange: (annotations: Annotation[]) => void;
refreshAnnotationData: (payload: Payload) => void;
theme?: SupersetTheme;
}
export interface PopoverState {
popoverVisible: Record;
addedAnnotationIndex: number | null;
}
function AnnotationLayerControl({
colorScheme,
annotationError = {},
annotationQuery = {},
vizType = '',
validationErrors,
name,
actions,
value = [],
onChange = () => {},
refreshAnnotationData,
}: Props) {
const theme = useTheme();
const [popoverVisible, setPopoverVisible] = useState<
Record
>({});
const [addedAnnotationIndex, setAddedAnnotationIndex] = useState<
number | null
>(null);
// componentDidMount - preload the AnnotationLayer component and dependent libraries i.e. mathjs
useEffect(() => {
AnnotationLayer.preload();
}, []);
// componentDidUpdate - sync validation errors
useEffect(() => {
if (
(Object.keys(annotationError).length && !validationErrors.length) ||
(!Object.keys(annotationError).length && validationErrors.length)
) {
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 (
addAnnotationLayer(annotation, newAnnotation)
}
removeAnnotationLayer={() => removeAnnotationLayer(annotation)}
close={() => {
handleVisibleChange(false, popoverKey);
setAddedAnnotationIndex(null);
}}
/>
);
},
[
colorScheme,
vizType,
addAnnotationLayer,
removeAnnotationLayer,
handleVisibleChange,
],
);
const renderInfo = useCallback(
(anno: Annotation) => {
if (annotationQuery[anno.name]) {
return (
);
}
if (annotationError[anno.name]) {
return (
);
}
if (!anno.show) {
return {t('Hidden')} ;
}
return '';
},
[annotationQuery, annotationError, theme],
);
const addedAnnotation = useMemo(
() => (addedAnnotationIndex !== null ? value[addedAnnotationIndex] : null),
[addedAnnotationIndex, value],
);
const annotations = value.map((anno, i) => (
({
'&:hover': {
cursor: 'pointer',
backgroundColor: thm.colorFillContentHover,
},
})}
content={renderPopover(i, anno, annotationError[anno.name])}
open={popoverVisible[i]}
onOpenChange={visible => handleVisibleChange(visible, i)}
>
{anno.name}
{renderInfo(anno)}
));
const addLayerPopoverKey = 'add';
return (
({ borderRadius: thm.borderRadius })}>
{annotations}
handleVisibleChange(visible, addLayerPopoverKey)
}
>
{t('Add annotation layer')}
);
}
// Tried to hook this up through stores/control.jsx instead of using redux
// directly, could not figure out how to get access to the color_scheme
function mapStateToProps({
charts,
explore,
}: Pick) {
const chartKey = getChartKey(explore);
const defaultChartState: Partial = {
annotationError: {},
annotationQuery: {},
};
const chart =
chartKey && charts[chartKey] ? charts[chartKey] : defaultChartState;
return {
// eslint-disable-next-line camelcase
colorScheme: explore.controls?.color_scheme?.value,
annotationError: chart.annotationError ?? {},
annotationQuery: chart.annotationQuery ?? {},
vizType: explore.controls?.viz_type.value,
};
}
function mapDispatchToProps(
dispatch: ThunkDispatch,
) {
return {
// Note: There's a type mismatch between the local Annotation interface
// and RunAnnotationQueryParams. This cast preserves existing runtime behavior.
refreshAnnotationData: (annotation: Annotation) =>
dispatch(
runAnnotationQuery(
annotation as unknown as Parameters[0],
),
),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationLayerControl);