mirror of
https://github.com/apache/superset.git
synced 2026-07-09 08:15:49 +00:00
Flip @typescript-eslint/no-unused-vars from "warn" to "error" in oxlint.json (with argsIgnorePattern/varsIgnorePattern of "^_" and caughtErrors "none"), and clean up the resulting violations across the frontend. Fixes favor proper cleanup over underscore hacks: genuinely dead imports, variables, and destructured props are removed; parameters are only "_"-prefixed when their position is required by an external or callback signature (event handlers, render props, antd validators, forwardRef, or omit-from-rest destructuring). Note: the .eslintrc.js half of the prior attempt (#37886) is obsolete since the frontend migrated to oxlint as the primary linter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
367 lines
10 KiB
TypeScript
367 lines
10 KiB
TypeScript
/**
|
|
* 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 { FunctionComponent, useState, useEffect, ChangeEvent } from 'react';
|
|
|
|
import { t } from '@apache-superset/core/translation';
|
|
import { styled } from '@apache-superset/core/theme';
|
|
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
|
import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
|
import withToasts from 'src/components/MessageToasts/withToasts';
|
|
import { Input, Modal, RangePicker } from '@superset-ui/core/components';
|
|
import { useJsonValidation } from '@superset-ui/core/components/AsyncAceEditor';
|
|
import type { editors } from '@apache-superset/core';
|
|
import { EditorHost } from 'src/core/editors';
|
|
import { OnlyKeyWithType } from 'src/utils/types';
|
|
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
|
|
import { AnnotationObject } from './types';
|
|
|
|
type EditorAnnotation = editors.EditorAnnotation;
|
|
|
|
/**
|
|
* Convert Ace annotation format to EditorAnnotation format.
|
|
*/
|
|
const toEditorAnnotations = (
|
|
aceAnnotations: Array<{
|
|
type: string;
|
|
row: number;
|
|
column: number;
|
|
text: string;
|
|
}>,
|
|
): EditorAnnotation[] =>
|
|
aceAnnotations.map(ann => ({
|
|
severity: ann.type as EditorAnnotation['severity'],
|
|
line: ann.row,
|
|
column: ann.column,
|
|
message: ann.text,
|
|
}));
|
|
|
|
interface AnnotationModalProps {
|
|
addDangerToast: (msg: string) => void;
|
|
addSuccessToast: (msg: string) => void;
|
|
annotationLayerId: number;
|
|
annotation?: AnnotationObject | null;
|
|
onAnnotationAdd?: (annotation?: AnnotationObject) => void;
|
|
onHide: () => void;
|
|
show: boolean;
|
|
}
|
|
|
|
const StyledAnnotationTitle = styled.div`
|
|
margin: ${({ theme }) => theme.sizeUnit * 2}px auto
|
|
${({ theme }) => theme.sizeUnit * 4}px auto;
|
|
`;
|
|
|
|
const StyledEditorHost = styled(EditorHost)`
|
|
/* Border is already applied by AceEditor itself */
|
|
`;
|
|
|
|
const AnnotationContainer = styled.div`
|
|
margin-bottom: ${({ theme }) => theme.sizeUnit * 5}px;
|
|
|
|
.control-label {
|
|
margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
|
|
}
|
|
|
|
.required {
|
|
margin-left: ${({ theme }) => theme.sizeUnit / 2}px;
|
|
color: ${({ theme }) => theme.colorError};
|
|
}
|
|
|
|
input[type='text'] {
|
|
width: 65%;
|
|
}
|
|
`;
|
|
|
|
const AnnotationModal: FunctionComponent<AnnotationModalProps> = ({
|
|
addDangerToast,
|
|
addSuccessToast,
|
|
annotationLayerId,
|
|
annotation = null,
|
|
onAnnotationAdd,
|
|
onHide,
|
|
show,
|
|
}) => {
|
|
const [disableSave, setDisableSave] = useState<boolean>(true);
|
|
const [currentAnnotation, setCurrentAnnotation] =
|
|
useState<AnnotationObject | null>(null);
|
|
const isEditMode = annotation !== null;
|
|
|
|
// JSON validation for metadata
|
|
const jsonAnnotations = useJsonValidation(currentAnnotation?.json_metadata, {
|
|
errorPrefix: 'Invalid JSON metadata',
|
|
});
|
|
|
|
// annotation fetch logic
|
|
const {
|
|
state: { loading, resource },
|
|
fetchResource,
|
|
createResource,
|
|
updateResource,
|
|
} = useSingleViewResource<AnnotationObject>(
|
|
`annotation_layer/${annotationLayerId}/annotation`,
|
|
t('annotation'),
|
|
addDangerToast,
|
|
);
|
|
|
|
const resetAnnotation = () => {
|
|
// Reset annotation
|
|
setCurrentAnnotation({
|
|
short_descr: '',
|
|
start_dttm: '',
|
|
end_dttm: '',
|
|
json_metadata: '',
|
|
long_descr: '',
|
|
});
|
|
};
|
|
|
|
const hide = () => {
|
|
if (isEditMode) {
|
|
setCurrentAnnotation(resource);
|
|
} else {
|
|
resetAnnotation();
|
|
}
|
|
onHide();
|
|
};
|
|
|
|
const onSave = () => {
|
|
if (isEditMode) {
|
|
// Edit
|
|
if (currentAnnotation?.id) {
|
|
const update_id = currentAnnotation.id;
|
|
delete currentAnnotation.id;
|
|
delete currentAnnotation.created_by;
|
|
delete currentAnnotation.changed_by;
|
|
delete currentAnnotation.changed_on_delta_humanized;
|
|
delete currentAnnotation.layer;
|
|
updateResource(update_id, currentAnnotation).then(response => {
|
|
// No response on error
|
|
if (!response) {
|
|
return;
|
|
}
|
|
|
|
if (onAnnotationAdd) {
|
|
onAnnotationAdd();
|
|
}
|
|
|
|
hide();
|
|
|
|
addSuccessToast(t('The annotation has been updated'));
|
|
});
|
|
}
|
|
} else if (currentAnnotation) {
|
|
// Create
|
|
createResource(currentAnnotation).then(response => {
|
|
if (!response) {
|
|
return;
|
|
}
|
|
|
|
if (onAnnotationAdd) {
|
|
onAnnotationAdd();
|
|
}
|
|
|
|
hide();
|
|
|
|
addSuccessToast(t('The annotation has been saved'));
|
|
});
|
|
}
|
|
};
|
|
|
|
const onAnnotationTextChange = (
|
|
event: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>,
|
|
) => {
|
|
const { target } = event;
|
|
|
|
const data = {
|
|
...currentAnnotation,
|
|
end_dttm: currentAnnotation ? currentAnnotation.end_dttm : '',
|
|
short_descr: currentAnnotation ? currentAnnotation.short_descr : '',
|
|
start_dttm: currentAnnotation ? currentAnnotation.start_dttm : '',
|
|
};
|
|
|
|
data[target.name as OnlyKeyWithType<typeof data, string>] = target.value;
|
|
setCurrentAnnotation(data);
|
|
};
|
|
|
|
const onJsonChange = (json: string) => {
|
|
const data = {
|
|
...currentAnnotation,
|
|
end_dttm: currentAnnotation ? currentAnnotation.end_dttm : '',
|
|
json_metadata: json,
|
|
short_descr: currentAnnotation ? currentAnnotation.short_descr : '',
|
|
start_dttm: currentAnnotation ? currentAnnotation.start_dttm : '',
|
|
};
|
|
setCurrentAnnotation(data);
|
|
};
|
|
|
|
const onDateChange = (dates: any, _dateString: Array<string>) => {
|
|
if (!dates?.[0] || !dates?.[1]) {
|
|
const data = {
|
|
...currentAnnotation,
|
|
start_dttm: '',
|
|
end_dttm: '',
|
|
short_descr: currentAnnotation?.short_descr ?? '',
|
|
};
|
|
setCurrentAnnotation(data);
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
...currentAnnotation,
|
|
start_dttm: dates[0].format('YYYY-MM-DD HH:mm'),
|
|
end_dttm: dates[1].format('YYYY-MM-DD HH:mm'),
|
|
short_descr: currentAnnotation?.short_descr ?? '',
|
|
};
|
|
setCurrentAnnotation(data);
|
|
};
|
|
|
|
const validate = () => {
|
|
if (
|
|
currentAnnotation?.short_descr?.length &&
|
|
currentAnnotation?.start_dttm?.length &&
|
|
currentAnnotation?.end_dttm?.length
|
|
) {
|
|
setDisableSave(false);
|
|
} else {
|
|
setDisableSave(true);
|
|
}
|
|
};
|
|
|
|
// Initialize
|
|
useEffect(() => {
|
|
if (
|
|
isEditMode &&
|
|
(!currentAnnotation?.id ||
|
|
(annotation && annotation.id !== currentAnnotation.id) ||
|
|
show)
|
|
) {
|
|
if (annotation?.id !== null && !loading) {
|
|
const id = annotation.id || 0;
|
|
|
|
fetchResource(id);
|
|
}
|
|
} else if (
|
|
!isEditMode &&
|
|
(!currentAnnotation || currentAnnotation.id || show)
|
|
) {
|
|
resetAnnotation();
|
|
}
|
|
}, [annotation]);
|
|
|
|
useEffect(() => {
|
|
if (resource) {
|
|
setCurrentAnnotation(resource);
|
|
}
|
|
}, [resource]);
|
|
|
|
// Validation
|
|
useEffect(() => {
|
|
validate();
|
|
}, [
|
|
currentAnnotation ? currentAnnotation.short_descr : '',
|
|
currentAnnotation ? currentAnnotation.start_dttm : '',
|
|
currentAnnotation ? currentAnnotation.end_dttm : '',
|
|
]);
|
|
|
|
return (
|
|
<Modal
|
|
disablePrimaryButton={disableSave}
|
|
onHandledPrimaryAction={onSave}
|
|
onHide={hide}
|
|
primaryButtonName={isEditMode ? t('Save') : t('Add')}
|
|
show={show}
|
|
width="55%"
|
|
name={isEditMode ? t('Edit annotation') : t('Add annotation')}
|
|
title={
|
|
<ModalTitleWithIcon
|
|
data-test="annotation-modal-title"
|
|
isEditMode={isEditMode}
|
|
title={isEditMode ? t('Edit annotation') : t('Add annotation')}
|
|
/>
|
|
}
|
|
>
|
|
<StyledAnnotationTitle>
|
|
<h4>{t('General information')}</h4>
|
|
</StyledAnnotationTitle>
|
|
<AnnotationContainer>
|
|
<div className="control-label">
|
|
{t('Name')}
|
|
<span className="required">*</span>
|
|
</div>
|
|
<Input
|
|
name="short_descr"
|
|
onChange={onAnnotationTextChange}
|
|
type="text"
|
|
value={currentAnnotation?.short_descr}
|
|
/>
|
|
</AnnotationContainer>
|
|
<AnnotationContainer>
|
|
<div className="control-label">
|
|
{t('date')}
|
|
<span className="required">*</span>
|
|
</div>
|
|
<RangePicker
|
|
placeholder={[t('Start date'), t('End date')]}
|
|
format="YYYY-MM-DD HH:mm"
|
|
onCalendarChange={onDateChange}
|
|
showTime={{ format: 'hh:mm a' }}
|
|
use12Hours
|
|
value={
|
|
currentAnnotation?.start_dttm?.length ||
|
|
currentAnnotation?.end_dttm?.length
|
|
? [
|
|
extendedDayjs(currentAnnotation.start_dttm),
|
|
extendedDayjs(currentAnnotation.end_dttm),
|
|
]
|
|
: null
|
|
}
|
|
/>
|
|
</AnnotationContainer>
|
|
<StyledAnnotationTitle>
|
|
<h4>{t('Additional information')}</h4>
|
|
</StyledAnnotationTitle>
|
|
<AnnotationContainer>
|
|
<div className="control-label">{t('description')}</div>
|
|
<Input.TextArea
|
|
name="long_descr"
|
|
value={currentAnnotation ? currentAnnotation.long_descr : ''}
|
|
placeholder={t('Description (this can be seen in the list)')}
|
|
onChange={onAnnotationTextChange}
|
|
/>
|
|
</AnnotationContainer>
|
|
<AnnotationContainer>
|
|
<div className="control-label">{t('JSON metadata')}</div>
|
|
<StyledEditorHost
|
|
id="annotation-json-metadata"
|
|
onChange={onJsonChange}
|
|
value={
|
|
currentAnnotation?.json_metadata
|
|
? currentAnnotation.json_metadata
|
|
: ''
|
|
}
|
|
language="json"
|
|
width="100%"
|
|
height="120px"
|
|
annotations={toEditorAnnotations(jsonAnnotations)}
|
|
/>
|
|
</AnnotationContainer>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default withToasts(AnnotationModal);
|