import React from 'react'; import PropTypes from 'prop-types'; import { CompactPicker } from 'react-color'; import { Button } from 'react-bootstrap'; import $ from 'jquery'; import mathjs from 'mathjs'; import SelectControl from './SelectControl'; import TextControl from './TextControl'; import CheckboxControl from './CheckboxControl'; import AnnotationTypes, { DEFAULT_ANNOTATION_TYPE, ANNOTATION_SOURCE_TYPES, getAnnotationSourceTypeLabels, getAnnotationTypeLabel, getSupportedSourceTypes, getSupportedAnnotationTypes, requiresQuery, } from '../../../modules/AnnotationTypes'; import { ALL_COLOR_SCHEMES } from '../../../modules/colors'; import PopoverSection from '../../../components/PopoverSection'; import ControlHeader from '../ControlHeader'; import { nonEmpty } from '../../validators'; import vizTypes from '../../stores/visTypes'; 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, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), overrides: PropTypes.object, show: 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, overrides: {}, colorScheme: 'd3Category10', show: true, titleColumn: '', descriptionColumns: [], timeColumn: '', intervalEndColumn: '', addAnnotationLayer: () => {}, removeAnnotationLayer: () => {}, close: () => {}, }; export default class AnnotationLayer extends React.PureComponent { constructor(props) { super(props); const { name, annotationType, sourceType, color, opacity, style, width, value, overrides, show, titleColumn, descriptionColumns, timeColumn, intervalEndColumn } = props; this.state = { // base name, oldName: !this.props.name ? null : name, annotationType, sourceType, value, overrides, show, // slice titleColumn, descriptionColumns, timeColumn, intervalEndColumn, // display color: color || AUTOMATIC_COLOR, opacity, style, width, // refData isNew: !this.props.name, isLoadingOptions: true, valueOptions: [], validationErrors: {}, }; 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); } } isValidFormula(value, annotationType) { if (annotationType === AnnotationTypes.FORMULA) { try { mathjs.parse(value).compile().eval({ x: 0 }); } catch (err) { return true; } } return false; } isValidForm() { const { name, annotationType, sourceType, value, timeColumn, intervalEndColumn, } = this.state; const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)]; if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) { if (annotationType === AnnotationTypes.EVENT) { errors.push(nonEmpty(timeColumn)); } if (annotationType === AnnotationTypes.INTERVAL) { errors.push(nonEmpty(timeColumn)); errors.push(nonEmpty(intervalEndColumn)); } } errors.push(this.isValidFormula(value, annotationType)); return !errors.filter(x => x).length; } handleAnnotationType(annotationType) { this.setState({ annotationType, sourceType: null, validationErrors: {}, value: null, }); } handleAnnotationSourceType(sourceType) { this.setState({ sourceType, isLoadingOptions: true, validationErrors: {}, value: null, }); } handleValue(value) { this.setState({ value, descriptionColumns: null, intervalEndColumn: null, timeColumn: null, titleColumn: null, overrides: { since: null, until: null }, }); } fetchOptions(annotationType, sourceType, isLoadingOptions) { if (isLoadingOptions === true) { if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { $.ajax({ type: 'GET', url: '/annotationlayermodelview/api/read?', }).then((data) => { const layers = data ? data.result.map(layer => ({ value: layer.id, label: layer.name, })) : []; this.setState({ isLoadingOptions: false, valueOptions: layers, }); }); } else if (requiresQuery(sourceType)) { $.ajax({ type: 'GET', url: '/superset/user_slices', }).then(data => this.setState({ isLoadingOptions: false, valueOptions: data.filter( x => getSupportedSourceTypes(annotationType) .find(v => v === x.viz_type)) .map(x => ({ value: x.id, label: x.title, slice: x }), ), }), ); } else { this.setState({ isLoadingOptions: false, valueOptions: [], }); } } } deleteAnnotation() { this.props.close(); if (!this.state.isNew) { this.props.removeAnnotationLayer(this.state); } } applyAnnotation() { if (this.state.name.length) { const annotation = {}; Object.keys(this.state).forEach((k) => { if (this.state[k] !== null) { annotation[k] = this.state[k]; } }); delete annotation.isNew; delete annotation.valueOptions; delete annotation.isLoadingOptions; delete annotation.validationErrors; annotation.color = annotation.color === AUTOMATIC_COLOR ? null : annotation.color; this.props.addAnnotationLayer(annotation); this.setState({ isNew: false, oldName: this.state.name }); } } submitAnnotation() { this.applyAnnotation(); this.props.close(); } renderValueConfiguration() { const { annotationType, sourceType, value, valueOptions, isLoadingOptions } = this.state; let label = ''; let description = ''; if (requiresQuery(sourceType)) { if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { label = 'Annotation Layer'; description = 'Select the Annotation Layer you would like to use.'; } else { label = 'Slice'; description = `Use a pre defined Superset Slice as a source for annotations and overlays. 'your Slice must be one of these visualization types: '[${getSupportedSourceTypes(sourceType) .map(x => vizTypes[x].label).join(', ')}]'`; } } else if (annotationType === AnnotationTypes.FORMULA) { label = 'Formula'; description = `Expects a formula with depending time parameter 'x' in milliseconds since epoch. mathjs is used to evaluate the formulas. Example: '2x+5'`; } if (requiresQuery(sourceType)) { return ( ); } if (annotationType === AnnotationTypes.FORMULA) { return ( ); } return ''; } renderSliceConfiguration() { const { annotationType, sourceType, value, valueOptions, overrides, titleColumn, timeColumn, intervalEndColumn, descriptionColumns } = this.state; const slice = (valueOptions.find(x => x.value === value) || {}).slice; if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) { const columns = (slice.data.groupby || []).concat( (slice.data.all_columns || [])).map(x => ({ value: x, label: x })); const timeColumnOptions = slice.data.include_time ? [{ value: '__timestamp', label: '__timestamp' }].concat(columns) : columns; return (
{ }} title="Annotation Slice Configuration" info={ `This section allows you to configure how to use the slice to generate annotations.` } > { ( annotationType === AnnotationTypes.EVENT || annotationType === AnnotationTypes.INTERVAL ) && this.setState({ timeColumn: v })} /> } { annotationType === AnnotationTypes.INTERVAL && this.setState({ intervalEndColumn: v })} /> } this.setState({ titleColumn: v })} /> { annotationType !== AnnotationTypes.TIME_SERIES && this.setState({ descriptionColumns: v })} /> }
x === 'since')} onChange={(v) => { delete overrides.since; if (v) { this.setState({ overrides: { ...overrides, since: null } }); } else { this.setState({ overrides: { ...overrides } }); } }} /> x === 'until')} onChange={(v) => { delete overrides.until; if (v) { this.setState({ overrides: { ...overrides, until: null } }); } else { this.setState({ overrides: { ...overrides } }); } }} /> this.setState({ overrides: { ...overrides, time_shift: v } })} />
); } return (''); } renderDisplayConfiguration() { const { color, opacity, style, width } = this.state; const colorScheme = [...ALL_COLOR_SCHEMES[this.props.colorScheme]]; if (color && color !== AUTOMATIC_COLOR && !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) { colorScheme.push(color); } return ( {}} title="Display configuration" info="Configure your how you overlay is displayed here." > this.setState({ style: v })} /> this.setState({ opacity: v })} />
this.setState({ color: v.hex })} />
this.setState({ width: v })} />
); } render() { const { isNew, name, annotationType, sourceType, show } = this.state; const isValid = this.isValidForm(); return (
{ this.props.error && ERROR: {this.props.error} }
{}} title="Layer Configuration" info="Configure the basics of your Annotation Layer." > this.setState({ name: v })} validationErrors={!name ? ['Mandatory'] : []} /> this.setState({ show: !v })} /> ({ value: x, label: getAnnotationTypeLabel(x) }))} value={annotationType} onChange={this.handleAnnotationType} /> {!!getSupportedSourceTypes(annotationType).length && ({ value: x, label: getAnnotationSourceTypeLabels(x) }))} value={sourceType} onChange={this.handleAnnotationSourceType} /> } { this.renderValueConfiguration() }
{ this.renderSliceConfiguration() } { this.renderDisplayConfiguration() }
); } } AnnotationLayer.propTypes = propTypes; AnnotationLayer.defaultProps = defaultProps;