mirror of
https://github.com/apache/superset.git
synced 2026-06-01 05:39:17 +00:00
chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript (#36760)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
selectOption,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
@@ -210,39 +211,23 @@ test('fetches chart on mount if value present', async () => {
|
||||
});
|
||||
|
||||
test('keeps apply disabled when missing required fields', async () => {
|
||||
// With EVENT type and Table source, the component requires selecting a chart
|
||||
// and filling in required fields. Without completing these, Apply should be disabled.
|
||||
await waitForRender({
|
||||
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
|
||||
sourceType: 'Table',
|
||||
});
|
||||
userEvent.click(
|
||||
screen.getByRole('combobox', { name: 'Annotation layer value' }),
|
||||
);
|
||||
expect(await screen.findByText('Chart A')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('Chart A'));
|
||||
|
||||
// Apply button should be disabled initially since required fields are not filled
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
|
||||
|
||||
// Select Chart A from the annotation layer value dropdown
|
||||
await selectOption('Chart A', 'Annotation layer value');
|
||||
|
||||
// Wait for the chart data to load
|
||||
await screen.findByText(/title column/i);
|
||||
userEvent.click(
|
||||
screen.getByRole('combobox', { name: 'Annotation layer title column' }),
|
||||
);
|
||||
expect(await screen.findByText(/none/i)).toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('None'));
|
||||
userEvent.click(screen.getByText('Style'));
|
||||
// The checkbox for automatic color is in the Style tab
|
||||
userEvent.click(screen.getByText('Use automatic color'));
|
||||
userEvent.click(
|
||||
screen.getByRole('combobox', { name: 'Annotation layer stroke' }),
|
||||
);
|
||||
expect(await screen.findByText('Dashed')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('Dashed'));
|
||||
userEvent.click(screen.getByText('Opacity'));
|
||||
userEvent.click(
|
||||
screen.getByRole('combobox', { name: 'Annotation layer opacity' }),
|
||||
);
|
||||
expect(await screen.findByText(/0.5/i)).toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('0.5'));
|
||||
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
checkboxes.forEach(checkbox => userEvent.click(checkbox));
|
||||
|
||||
// Apply should still be disabled because name is not filled
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import rison from 'rison';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
AsyncSelect,
|
||||
@@ -34,8 +33,13 @@ import {
|
||||
isValidExpression,
|
||||
getColumnLabel,
|
||||
VizType,
|
||||
type QueryFormColumn,
|
||||
} from '@superset-ui/core';
|
||||
import { styled, withTheme } from '@apache-superset/core/ui';
|
||||
import {
|
||||
styled,
|
||||
withTheme,
|
||||
type SupersetTheme,
|
||||
} from '@apache-superset/core/ui';
|
||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||
import TextControl from 'src/explore/components/controls/TextControl';
|
||||
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
||||
@@ -50,60 +54,81 @@ import {
|
||||
ANNOTATION_SOURCE_TYPES_METADATA,
|
||||
} from './AnnotationTypes';
|
||||
|
||||
interface SelectOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
viz_type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface SliceData {
|
||||
data: {
|
||||
groupby?: string[];
|
||||
all_columns?: string[];
|
||||
include_time?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
interface AnnotationOverrides {
|
||||
time_range?: string | null;
|
||||
time_grain_sqla?: string | null;
|
||||
granularity?: string | null;
|
||||
time_shift?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface AnnotationLayerProps {
|
||||
name?: string;
|
||||
annotationType?: string;
|
||||
sourceType?: string;
|
||||
color?: string;
|
||||
opacity?: string;
|
||||
style?: string;
|
||||
width?: number;
|
||||
showMarkers?: boolean;
|
||||
hideLine?: boolean;
|
||||
value?: string | number | SelectOption;
|
||||
overrides?: AnnotationOverrides;
|
||||
show?: boolean;
|
||||
showLabel?: boolean;
|
||||
titleColumn?: string;
|
||||
descriptionColumns?: string[];
|
||||
timeColumn?: string;
|
||||
intervalEndColumn?: string;
|
||||
vizType?: string;
|
||||
error?: string;
|
||||
colorScheme?: string;
|
||||
theme: SupersetTheme;
|
||||
addAnnotationLayer?: (annotation: Record<string, unknown>) => void;
|
||||
removeAnnotationLayer?: () => void;
|
||||
close?: () => void;
|
||||
}
|
||||
|
||||
interface AnnotationLayerState {
|
||||
name: string;
|
||||
annotationType: string;
|
||||
sourceType: string | null;
|
||||
value: string | number | SelectOption | null;
|
||||
overrides: AnnotationOverrides;
|
||||
show: boolean;
|
||||
showLabel: boolean;
|
||||
titleColumn: string;
|
||||
descriptionColumns: string[];
|
||||
timeColumn: string;
|
||||
intervalEndColumn: string;
|
||||
color: string;
|
||||
opacity: string;
|
||||
style: string;
|
||||
width: number;
|
||||
showMarkers: boolean;
|
||||
hideLine: boolean;
|
||||
isNew: boolean;
|
||||
slice: SliceData | null;
|
||||
}
|
||||
|
||||
const AUTOMATIC_COLOR = '';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string,
|
||||
annotationType: PropTypes.string,
|
||||
sourceType: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
opacity: PropTypes.string,
|
||||
style: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
showMarkers: PropTypes.bool,
|
||||
hideLine: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
overrides: PropTypes.object,
|
||||
show: PropTypes.bool,
|
||||
showLabel: PropTypes.bool,
|
||||
titleColumn: PropTypes.string,
|
||||
descriptionColumns: PropTypes.arrayOf(PropTypes.string),
|
||||
timeColumn: PropTypes.string,
|
||||
intervalEndColumn: PropTypes.string,
|
||||
vizType: PropTypes.string,
|
||||
|
||||
error: PropTypes.string,
|
||||
colorScheme: PropTypes.string,
|
||||
|
||||
addAnnotationLayer: PropTypes.func,
|
||||
removeAnnotationLayer: PropTypes.func,
|
||||
close: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
name: '',
|
||||
annotationType: DEFAULT_ANNOTATION_TYPE,
|
||||
sourceType: '',
|
||||
color: AUTOMATIC_COLOR,
|
||||
opacity: '',
|
||||
style: 'solid',
|
||||
width: 1,
|
||||
showMarkers: false,
|
||||
hideLine: false,
|
||||
overrides: {},
|
||||
colorScheme: 'd3Category10',
|
||||
show: true,
|
||||
showLabel: false,
|
||||
titleColumn: '',
|
||||
descriptionColumns: [],
|
||||
timeColumn: '',
|
||||
intervalEndColumn: '',
|
||||
|
||||
addAnnotationLayer: () => {},
|
||||
removeAnnotationLayer: () => {},
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
const NotFoundContentWrapper = styled.div`
|
||||
&& > div:first-child {
|
||||
padding-left: 0;
|
||||
@@ -134,8 +159,34 @@ const NotFoundContent = () => (
|
||||
</NotFoundContentWrapper>
|
||||
);
|
||||
|
||||
class AnnotationLayer extends PureComponent {
|
||||
constructor(props) {
|
||||
class AnnotationLayer extends PureComponent<
|
||||
AnnotationLayerProps,
|
||||
AnnotationLayerState
|
||||
> {
|
||||
static defaultProps = {
|
||||
name: '',
|
||||
annotationType: DEFAULT_ANNOTATION_TYPE,
|
||||
sourceType: '',
|
||||
color: AUTOMATIC_COLOR,
|
||||
opacity: '',
|
||||
style: 'solid',
|
||||
width: 1,
|
||||
showMarkers: false,
|
||||
hideLine: false,
|
||||
overrides: {},
|
||||
colorScheme: 'd3Category10',
|
||||
show: true,
|
||||
showLabel: false,
|
||||
titleColumn: '',
|
||||
descriptionColumns: [],
|
||||
timeColumn: '',
|
||||
intervalEndColumn: '',
|
||||
addAnnotationLayer: () => {},
|
||||
removeAnnotationLayer: () => {},
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
constructor(props: AnnotationLayerProps) {
|
||||
super(props);
|
||||
const {
|
||||
name,
|
||||
@@ -159,42 +210,46 @@ class AnnotationLayer extends PureComponent {
|
||||
} = props;
|
||||
|
||||
// Only allow override whole time_range
|
||||
if ('since' in overrides || 'until' in overrides) {
|
||||
overrides.time_range = null;
|
||||
delete overrides.since;
|
||||
delete overrides.until;
|
||||
const processedOverrides: AnnotationOverrides = overrides
|
||||
? { ...overrides }
|
||||
: {};
|
||||
if ('since' in processedOverrides || 'until' in processedOverrides) {
|
||||
processedOverrides.time_range = null;
|
||||
delete processedOverrides.since;
|
||||
delete processedOverrides.until;
|
||||
}
|
||||
|
||||
// Check if annotationType is supported by this chart
|
||||
const metadata = getChartMetadataRegistry().get(vizType);
|
||||
const metadata = vizType ? getChartMetadataRegistry().get(vizType) : null;
|
||||
const supportedAnnotationTypes = metadata?.supportedAnnotationTypes || [];
|
||||
const resolvedAnnotationType = annotationType || DEFAULT_ANNOTATION_TYPE;
|
||||
const validAnnotationType = supportedAnnotationTypes.includes(
|
||||
annotationType,
|
||||
resolvedAnnotationType,
|
||||
)
|
||||
? annotationType
|
||||
? resolvedAnnotationType
|
||||
: supportedAnnotationTypes[0];
|
||||
|
||||
this.state = {
|
||||
// base
|
||||
name,
|
||||
annotationType: validAnnotationType,
|
||||
sourceType,
|
||||
value,
|
||||
overrides,
|
||||
show,
|
||||
showLabel,
|
||||
name: name || '',
|
||||
annotationType: validAnnotationType || DEFAULT_ANNOTATION_TYPE,
|
||||
sourceType: sourceType || null,
|
||||
value: value || null,
|
||||
overrides: processedOverrides,
|
||||
show: show ?? true,
|
||||
showLabel: showLabel ?? false,
|
||||
// slice
|
||||
titleColumn,
|
||||
descriptionColumns,
|
||||
timeColumn,
|
||||
intervalEndColumn,
|
||||
titleColumn: titleColumn || '',
|
||||
descriptionColumns: descriptionColumns || [],
|
||||
timeColumn: timeColumn || '',
|
||||
intervalEndColumn: intervalEndColumn || '',
|
||||
// display
|
||||
color: color || AUTOMATIC_COLOR,
|
||||
opacity,
|
||||
style,
|
||||
width,
|
||||
showMarkers,
|
||||
hideLine,
|
||||
opacity: opacity || '',
|
||||
style: style || 'solid',
|
||||
width: width ?? 1,
|
||||
showMarkers: showMarkers ?? false,
|
||||
hideLine: hideLine ?? false,
|
||||
// refData
|
||||
isNew: !name,
|
||||
slice: null,
|
||||
@@ -229,57 +284,71 @@ class AnnotationLayer extends PureComponent {
|
||||
/* The value prop is the id of the chart/native. This function will set
|
||||
value in state to an object with the id as value.value to be used by
|
||||
AsyncSelect */
|
||||
this.fetchAppliedAnnotation(value);
|
||||
if (value !== null && typeof value !== 'object') {
|
||||
this.fetchAppliedAnnotation(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
componentDidUpdate(
|
||||
_prevProps: AnnotationLayerProps,
|
||||
prevState: AnnotationLayerState,
|
||||
): void {
|
||||
if (this.shouldFetchSliceData(prevState)) {
|
||||
const { value } = this.state;
|
||||
this.fetchSliceData(value.value);
|
||||
if (value && typeof value === 'object' && 'value' in value) {
|
||||
this.fetchSliceData(value.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSupportedSourceTypes(annotationType) {
|
||||
getSupportedSourceTypes(annotationType: string): SelectOption[] {
|
||||
// Get vis types that can be source.
|
||||
const sources = getChartMetadataRegistry()
|
||||
.entries()
|
||||
.filter(({ value: chartMetadata }) =>
|
||||
chartMetadata.canBeAnnotationType(annotationType),
|
||||
chartMetadata?.canBeAnnotationType(annotationType),
|
||||
)
|
||||
.map(({ key, value: chartMetadata }) => ({
|
||||
value: key === VizType.Line ? 'line' : key,
|
||||
label: chartMetadata.name,
|
||||
label: chartMetadata?.name || key,
|
||||
}));
|
||||
// Prepend native source if applicable
|
||||
if (ANNOTATION_TYPES_METADATA[annotationType]?.supportNativeSource) {
|
||||
const annotationMeta =
|
||||
ANNOTATION_TYPES_METADATA[
|
||||
annotationType as keyof typeof ANNOTATION_TYPES_METADATA
|
||||
];
|
||||
if (annotationMeta && 'supportNativeSource' in annotationMeta) {
|
||||
sources.unshift(ANNOTATION_SOURCE_TYPES_METADATA.NATIVE);
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
shouldFetchAppliedAnnotation() {
|
||||
shouldFetchAppliedAnnotation(): boolean {
|
||||
const { value, sourceType } = this.state;
|
||||
return value && requiresQuery(sourceType);
|
||||
return !!value && requiresQuery(sourceType ?? undefined);
|
||||
}
|
||||
|
||||
shouldFetchSliceData(prevState) {
|
||||
shouldFetchSliceData(prevState: AnnotationLayerState): boolean {
|
||||
const { value, sourceType } = this.state;
|
||||
const isChart =
|
||||
sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE &&
|
||||
requiresQuery(sourceType);
|
||||
requiresQuery(sourceType ?? undefined);
|
||||
const valueIsNew = value && prevState.value !== value;
|
||||
return valueIsNew && isChart;
|
||||
return !!valueIsNew && isChart;
|
||||
}
|
||||
|
||||
isValidFormulaAnnotation(expression, annotationType) {
|
||||
isValidFormulaAnnotation(
|
||||
expression: string | number | SelectOption | null,
|
||||
annotationType: string,
|
||||
): boolean {
|
||||
if (annotationType === ANNOTATION_TYPES.FORMULA) {
|
||||
return isValidExpression(expression);
|
||||
return isValidExpression(expression as string);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isValidForm() {
|
||||
isValidForm(): boolean {
|
||||
const {
|
||||
name,
|
||||
annotationType,
|
||||
@@ -302,11 +371,13 @@ class AnnotationLayer extends PureComponent {
|
||||
errors.push(validateNonEmpty(intervalEndColumn));
|
||||
}
|
||||
}
|
||||
errors.push(!this.isValidFormulaAnnotation(value, annotationType));
|
||||
if (!this.isValidFormulaAnnotation(value, annotationType)) {
|
||||
errors.push(t('Invalid formula expression'));
|
||||
}
|
||||
return !errors.filter(x => x).length;
|
||||
}
|
||||
|
||||
handleAnnotationType(annotationType) {
|
||||
handleAnnotationType(annotationType: string): void {
|
||||
this.setState({
|
||||
annotationType,
|
||||
sourceType: null,
|
||||
@@ -315,7 +386,7 @@ class AnnotationLayer extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
handleAnnotationSourceType(sourceType) {
|
||||
handleAnnotationSourceType(sourceType: string): void {
|
||||
const { sourceType: prevSourceType } = this.state;
|
||||
|
||||
if (prevSourceType !== sourceType) {
|
||||
@@ -327,24 +398,28 @@ class AnnotationLayer extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectValue(selectedValueObject) {
|
||||
handleSelectValue(selectedValueObject: SelectOption): void {
|
||||
this.setState({
|
||||
value: selectedValueObject,
|
||||
descriptionColumns: [],
|
||||
intervalEndColumn: null,
|
||||
timeColumn: null,
|
||||
titleColumn: null,
|
||||
intervalEndColumn: '',
|
||||
timeColumn: '',
|
||||
titleColumn: '',
|
||||
overrides: { time_range: null },
|
||||
});
|
||||
}
|
||||
|
||||
handleTextValue(inputValue) {
|
||||
handleTextValue(inputValue: string): void {
|
||||
this.setState({
|
||||
value: inputValue,
|
||||
});
|
||||
}
|
||||
|
||||
fetchNativeAnnotations = async (search, page, pageSize) => {
|
||||
fetchNativeAnnotations = async (
|
||||
search: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<{ data: SelectOption[]; totalCount: number }> => {
|
||||
const queryParams = rison.encode({
|
||||
filters: [
|
||||
{
|
||||
@@ -364,7 +439,7 @@ class AnnotationLayer extends PureComponent {
|
||||
|
||||
const { result, count } = json;
|
||||
|
||||
const layersArray = result.map(layer => ({
|
||||
const layersArray = result.map((layer: { id: number; name: string }) => ({
|
||||
value: layer.id,
|
||||
label: layer.name,
|
||||
}));
|
||||
@@ -375,7 +450,11 @@ class AnnotationLayer extends PureComponent {
|
||||
};
|
||||
};
|
||||
|
||||
fetchCharts = async (search, page, pageSize) => {
|
||||
fetchCharts = async (
|
||||
search: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<{ data: SelectOption[]; totalCount: number }> => {
|
||||
const { annotationType } = this.state;
|
||||
|
||||
const queryParams = rison.encode({
|
||||
@@ -401,11 +480,11 @@ class AnnotationLayer extends PureComponent {
|
||||
const registry = getChartMetadataRegistry();
|
||||
|
||||
const chartsArray = result
|
||||
.filter(chart => {
|
||||
.filter((chart: { id: number; slice_name: string; viz_type: string }) => {
|
||||
const metadata = registry.get(chart.viz_type);
|
||||
return metadata && metadata.canBeAnnotationType(annotationType);
|
||||
})
|
||||
.map(chart => ({
|
||||
.map((chart: { id: number; slice_name: string; viz_type: string }) => ({
|
||||
value: chart.id,
|
||||
label: chart.slice_name,
|
||||
viz_type: chart.viz_type,
|
||||
@@ -417,7 +496,11 @@ class AnnotationLayer extends PureComponent {
|
||||
};
|
||||
};
|
||||
|
||||
fetchOptions = (search, page, pageSize) => {
|
||||
fetchOptions = (
|
||||
search: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<{ data: SelectOption[]; totalCount: number }> => {
|
||||
const { sourceType } = this.state;
|
||||
|
||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
@@ -426,7 +509,7 @@ class AnnotationLayer extends PureComponent {
|
||||
return this.fetchCharts(search, page, pageSize);
|
||||
};
|
||||
|
||||
fetchSliceData = id => {
|
||||
fetchSliceData = (id: string | number): void => {
|
||||
const queryParams = rison.encode({
|
||||
columns: ['query_context'],
|
||||
});
|
||||
@@ -439,7 +522,9 @@ class AnnotationLayer extends PureComponent {
|
||||
const dataObject = {
|
||||
data: {
|
||||
...formData,
|
||||
groupby: formData.groupby?.map(column => getColumnLabel(column)),
|
||||
groupby: formData.groupby?.map((column: QueryFormColumn) =>
|
||||
getColumnLabel(column),
|
||||
),
|
||||
},
|
||||
};
|
||||
this.setState({
|
||||
@@ -448,7 +533,7 @@ class AnnotationLayer extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
fetchAppliedChart(id) {
|
||||
fetchAppliedChart(id: string | number): void {
|
||||
const { annotationType } = this.state;
|
||||
const registry = getChartMetadataRegistry();
|
||||
const queryParams = rison.encode({
|
||||
@@ -474,7 +559,9 @@ class AnnotationLayer extends PureComponent {
|
||||
slice: {
|
||||
data: {
|
||||
...formData,
|
||||
groupby: formData.groupby?.map(column => getColumnLabel(column)),
|
||||
groupby: formData.groupby?.map((column: QueryFormColumn) =>
|
||||
getColumnLabel(column),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -482,7 +569,7 @@ class AnnotationLayer extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
fetchAppliedNativeAnnotation(id) {
|
||||
fetchAppliedNativeAnnotation(id: string | number): void {
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/annotation_layer/${id}`,
|
||||
}).then(({ json }) => {
|
||||
@@ -497,7 +584,7 @@ class AnnotationLayer extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
fetchAppliedAnnotation(id) {
|
||||
fetchAppliedAnnotation(id: string | number): void {
|
||||
const { sourceType } = this.state;
|
||||
|
||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
@@ -506,12 +593,12 @@ class AnnotationLayer extends PureComponent {
|
||||
return this.fetchAppliedChart(id);
|
||||
}
|
||||
|
||||
deleteAnnotation() {
|
||||
this.props.removeAnnotationLayer();
|
||||
this.props.close();
|
||||
deleteAnnotation(): void {
|
||||
this.props.removeAnnotationLayer?.();
|
||||
this.props.close?.();
|
||||
}
|
||||
|
||||
applyAnnotation() {
|
||||
applyAnnotation(): void {
|
||||
const { value, sourceType } = this.state;
|
||||
if (this.isValidForm()) {
|
||||
const annotationFields = [
|
||||
@@ -532,32 +619,42 @@ class AnnotationLayer extends PureComponent {
|
||||
'timeColumn',
|
||||
'intervalEndColumn',
|
||||
];
|
||||
const newAnnotation = {};
|
||||
const newAnnotation: Record<string, unknown> = {};
|
||||
annotationFields.forEach(field => {
|
||||
if (this.state[field] !== null) {
|
||||
newAnnotation[field] = this.state[field];
|
||||
const stateValue = this.state[field as keyof AnnotationLayerState];
|
||||
if (stateValue !== null) {
|
||||
newAnnotation[field] = stateValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Prepare newAnnotation.value for use in runAnnotationQuery()
|
||||
const applicableValue = requiresQuery(sourceType) ? value.value : value;
|
||||
const applicableValue =
|
||||
requiresQuery(sourceType ?? undefined) &&
|
||||
value &&
|
||||
typeof value === 'object'
|
||||
? (value as SelectOption).value
|
||||
: value;
|
||||
newAnnotation.value = applicableValue;
|
||||
|
||||
if (newAnnotation.color === AUTOMATIC_COLOR) {
|
||||
newAnnotation.color = null;
|
||||
}
|
||||
|
||||
this.props.addAnnotationLayer(newAnnotation);
|
||||
this.props.addAnnotationLayer?.(newAnnotation);
|
||||
this.setState({ isNew: false });
|
||||
}
|
||||
}
|
||||
|
||||
submitAnnotation() {
|
||||
submitAnnotation(): void {
|
||||
this.applyAnnotation();
|
||||
this.props.close();
|
||||
this.props.close?.();
|
||||
}
|
||||
|
||||
renderChartHeader(label, description, value) {
|
||||
renderChartHeader(
|
||||
label: string,
|
||||
description: string,
|
||||
value: string | number | SelectOption | null,
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<ControlHeader
|
||||
hovered
|
||||
@@ -568,11 +665,11 @@ class AnnotationLayer extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderValueConfiguration() {
|
||||
renderValueConfiguration(): React.ReactNode {
|
||||
const { annotationType, sourceType, value } = this.state;
|
||||
let label = '';
|
||||
let description = '';
|
||||
if (requiresQuery(sourceType)) {
|
||||
if (requiresQuery(sourceType ?? undefined)) {
|
||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
label = t('Annotation layer');
|
||||
description = t('Select the Annotation Layer you would like to use.');
|
||||
@@ -592,7 +689,7 @@ class AnnotationLayer extends PureComponent {
|
||||
in milliseconds since epoch. mathjs is used to evaluate the formulas.
|
||||
Example: '2x+5'`);
|
||||
}
|
||||
if (requiresQuery(sourceType)) {
|
||||
if (requiresQuery(sourceType ?? undefined)) {
|
||||
return (
|
||||
<AsyncSelect
|
||||
/* key to force re-render on sourceType change */
|
||||
@@ -608,6 +705,8 @@ class AnnotationLayer extends PureComponent {
|
||||
);
|
||||
}
|
||||
if (annotationType === ANNOTATION_TYPES.FORMULA) {
|
||||
// Extract primitive value for TextControl (formula is always a string)
|
||||
const textValue = typeof value === 'object' ? null : value;
|
||||
return (
|
||||
<TextControl
|
||||
name="annotation-layer-value"
|
||||
@@ -616,7 +715,7 @@ class AnnotationLayer extends PureComponent {
|
||||
description={description}
|
||||
label={label}
|
||||
placeholder=""
|
||||
value={value}
|
||||
value={textValue}
|
||||
onChange={this.handleTextValue}
|
||||
validationErrors={
|
||||
!this.isValidFormulaAnnotation(value, annotationType)
|
||||
@@ -629,7 +728,7 @@ class AnnotationLayer extends PureComponent {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderSliceConfiguration() {
|
||||
renderSliceConfiguration(): React.ReactNode {
|
||||
const {
|
||||
annotationType,
|
||||
sourceType,
|
||||
@@ -679,7 +778,9 @@ class AnnotationLayer extends PureComponent {
|
||||
clearable={false}
|
||||
options={timeColumnOptions}
|
||||
value={timeColumn}
|
||||
onChange={v => this.setState({ timeColumn: v })}
|
||||
onChange={(
|
||||
v: string | number | (string | number)[] | null | undefined,
|
||||
) => this.setState({ timeColumn: String(v ?? '') })}
|
||||
/>
|
||||
)}
|
||||
{annotationType === ANNOTATION_TYPES.INTERVAL && (
|
||||
@@ -694,7 +795,14 @@ class AnnotationLayer extends PureComponent {
|
||||
validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
|
||||
options={columns}
|
||||
value={intervalEndColumn}
|
||||
onChange={value => this.setState({ intervalEndColumn: value })}
|
||||
onChange={(
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| (string | number)[]
|
||||
| null
|
||||
| undefined,
|
||||
) => this.setState({ intervalEndColumn: String(value ?? '') })}
|
||||
/>
|
||||
)}
|
||||
<SelectControl
|
||||
@@ -705,7 +813,9 @@ class AnnotationLayer extends PureComponent {
|
||||
description={t('Pick a title for you annotation.')}
|
||||
options={[{ value: '', label: t('None') }].concat(columns)}
|
||||
value={titleColumn}
|
||||
onChange={value => this.setState({ titleColumn: value })}
|
||||
onChange={(
|
||||
value: string | number | (string | number)[] | null | undefined,
|
||||
) => this.setState({ titleColumn: String(value ?? '') })}
|
||||
/>
|
||||
{annotationType !== ANNOTATION_TYPES.TIME_SERIES && (
|
||||
<SelectControl
|
||||
@@ -719,7 +829,17 @@ class AnnotationLayer extends PureComponent {
|
||||
multi
|
||||
options={columns}
|
||||
value={descriptionColumns}
|
||||
onChange={value => this.setState({ descriptionColumns: value })}
|
||||
onChange={(
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| (string | number)[]
|
||||
| null
|
||||
| undefined,
|
||||
) => {
|
||||
const cols = Array.isArray(value) ? value.map(String) : [];
|
||||
this.setState({ descriptionColumns: cols });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
@@ -784,7 +904,7 @@ class AnnotationLayer extends PureComponent {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderDisplayConfiguration() {
|
||||
renderDisplayConfiguration(): React.ReactNode {
|
||||
const {
|
||||
color,
|
||||
opacity,
|
||||
@@ -794,9 +914,10 @@ class AnnotationLayer extends PureComponent {
|
||||
hideLine,
|
||||
annotationType,
|
||||
} = this.state;
|
||||
const colorScheme = getCategoricalSchemeRegistry()
|
||||
.get(this.props.colorScheme)
|
||||
.colors.concat();
|
||||
const colorScheme =
|
||||
getCategoricalSchemeRegistry()
|
||||
.get(this.props.colorScheme)
|
||||
?.colors.concat() ?? [];
|
||||
if (
|
||||
color &&
|
||||
color !== AUTOMATIC_COLOR &&
|
||||
@@ -823,7 +944,9 @@ class AnnotationLayer extends PureComponent {
|
||||
]}
|
||||
value={style}
|
||||
clearable={false}
|
||||
onChange={v => this.setState({ style: v })}
|
||||
onChange={(
|
||||
v: string | number | (string | number)[] | null | undefined,
|
||||
) => this.setState({ style: String(v ?? 'solid') })}
|
||||
/>
|
||||
<SelectControl
|
||||
ariaLabel={t('Annotation layer opacity')}
|
||||
@@ -837,7 +960,9 @@ class AnnotationLayer extends PureComponent {
|
||||
{ value: 'opacityHigh', label: '0.8' },
|
||||
]}
|
||||
value={opacity}
|
||||
onChange={value => this.setState({ opacity: value })}
|
||||
onChange={(
|
||||
value: string | number | (string | number)[] | null | undefined,
|
||||
) => this.setState({ opacity: String(value ?? '') })}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
@@ -905,14 +1030,19 @@ class AnnotationLayer extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): React.ReactNode {
|
||||
const { isNew, name, annotationType, sourceType, show, showLabel } =
|
||||
this.state;
|
||||
const isValid = this.isValidForm();
|
||||
const metadata = getChartMetadataRegistry().get(this.props.vizType);
|
||||
const metadata = this.props.vizType
|
||||
? getChartMetadataRegistry().get(this.props.vizType)
|
||||
: null;
|
||||
const supportedAnnotationTypes = metadata
|
||||
? metadata.supportedAnnotationTypes.map(
|
||||
type => ANNOTATION_TYPES_METADATA[type],
|
||||
type =>
|
||||
ANNOTATION_TYPES_METADATA[
|
||||
type as keyof typeof ANNOTATION_TYPES_METADATA
|
||||
],
|
||||
)
|
||||
: [];
|
||||
const supportedSourceTypes = this.getSupportedSourceTypes(annotationType);
|
||||
@@ -989,7 +1119,7 @@ class AnnotationLayer extends PureComponent {
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="secondary"
|
||||
onClick={() => this.props.close()}
|
||||
onClick={() => this.props.close?.()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
@@ -1026,7 +1156,4 @@ class AnnotationLayer extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
AnnotationLayer.propTypes = propTypes;
|
||||
AnnotationLayer.defaultProps = defaultProps;
|
||||
|
||||
export default withTheme(AnnotationLayer);
|
||||
@@ -18,12 +18,25 @@
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
function extractTypes(metadata) {
|
||||
return Object.keys(metadata).reduce((prev, key) => {
|
||||
const result = prev;
|
||||
result[key] = key;
|
||||
return result;
|
||||
}, {});
|
||||
interface Annotation {
|
||||
sourceType?: string;
|
||||
timeColumn?: string;
|
||||
intervalEndColumn?: string;
|
||||
titleColumn?: string;
|
||||
descriptionColumns?: string[];
|
||||
}
|
||||
|
||||
function extractTypes<T extends Record<string, { value: string }>>(
|
||||
metadata: T,
|
||||
): Record<keyof T, string> {
|
||||
return Object.keys(metadata).reduce(
|
||||
(prev, key) => {
|
||||
const result = prev;
|
||||
result[key as keyof T] = key;
|
||||
return result;
|
||||
},
|
||||
{} as Record<keyof T, string>,
|
||||
);
|
||||
}
|
||||
|
||||
export const ANNOTATION_TYPES_METADATA = {
|
||||
@@ -62,7 +75,9 @@ export const ANNOTATION_SOURCE_TYPES = extractTypes(
|
||||
ANNOTATION_SOURCE_TYPES_METADATA,
|
||||
);
|
||||
|
||||
export function requiresQuery(annotationSourceType) {
|
||||
export function requiresQuery(
|
||||
annotationSourceType: string | undefined,
|
||||
): boolean {
|
||||
return !!annotationSourceType;
|
||||
}
|
||||
|
||||
@@ -71,11 +86,16 @@ const NATIVE_COLUMN_NAMES = {
|
||||
intervalEndColumn: 'end_dttm',
|
||||
titleColumn: 'short_descr',
|
||||
descriptionColumns: ['long_descr'],
|
||||
};
|
||||
} as const;
|
||||
|
||||
export function applyNativeColumns(annotation) {
|
||||
export function applyNativeColumns(annotation: Annotation): Annotation {
|
||||
if (annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
return { ...annotation, ...NATIVE_COLUMN_NAMES };
|
||||
return {
|
||||
...annotation,
|
||||
...NATIVE_COLUMN_NAMES,
|
||||
// Spread to convert readonly array to mutable
|
||||
descriptionColumns: [...NATIVE_COLUMN_NAMES.descriptionColumns],
|
||||
};
|
||||
}
|
||||
return annotation;
|
||||
}
|
||||
Reference in New Issue
Block a user