Files
superset2/superset/assets/javascripts/explore/components/controls/AnnotationLayer.jsx
fabianmenges 500e6256c0 Full Annotation Framework (#3518)
* Adding full Annotation Framework

* Viz types

* Re organizing native annotations

* liniting

* Bug fix

* Handle no data

* Cleanup

* Refactor slice form_data to data
2017-12-16 16:10:45 -08:00

603 lines
19 KiB
JavaScript

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: [],
};
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 = { ...this.state };
annotation.color = annotation.color === AUTOMATIC_COLOR ? null : annotation.color;
delete annotation.isNew;
delete annotation.valueOptions;
delete annotation.isLoadingOptions;
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 (
<SelectControl
name="annotation-layer-value"
showHeader
hovered
description={description}
label={label}
placeholder=""
options={valueOptions}
isLoading={isLoadingOptions}
value={value}
onChange={this.handleValue}
validationErrors={!value ? ['Mandatory'] : []}
/>
);
} if (annotationType === AnnotationTypes.FORMULA) {
return (
<TextControl
name="annotation-layer-value"
hovered
showHeader
description={description}
label={label}
placeholder=""
value={value}
onChange={this.handleValue}
validationErrors={this.isValidFormula(value, annotationType) ? ['Bad formula.'] : []}
/>
);
}
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 (
<div style={{ marginRight: '2rem' }}>
<PopoverSection
isSelected
onSelect={() => {
}}
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
) &&
<SelectControl
hovered
name="annotation-layer-time-column"
label={
annotationType === AnnotationTypes.INTERVAL ?
'Interval Start column' : 'Event Time Column'
}
description={'This column must contain date/time information.'}
validationErrors={!timeColumn ? ['Mandatory'] : []}
clearable={false}
options={timeColumnOptions}
value={timeColumn}
onChange={v => this.setState({ timeColumn: v })}
/>
}
{
annotationType === AnnotationTypes.INTERVAL &&
<SelectControl
hovered
name="annotation-layer-intervalEnd"
label="Interval End column"
description={'This column must contain date/time information.'}
validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
options={columns}
value={intervalEndColumn}
onChange={v => this.setState({ intervalEndColumn: v })}
/>
}
<SelectControl
hovered
name="annotation-layer-title"
label="Title Column"
description={'Pick a title for you annotation.'}
options={
[{ value: '', label: 'None' }].concat(columns)
}
value={titleColumn}
onChange={v => this.setState({ titleColumn: v })}
/>
{
annotationType !== AnnotationTypes.TIME_SERIES &&
<SelectControl
hovered
name="annotation-layer-title"
label="Description Columns"
description={`Pick one or more columns that should be shown in the
annotation. If you don't select a column all of them will be shown.`}
multi
options={
columns
}
value={descriptionColumns}
onChange={v => this.setState({ descriptionColumns: v })}
/>
}
<div style={{ marginTop: '1rem' }}>
<CheckboxControl
hovered
name="annotation-override-since"
label="Override 'Since'"
description={`This controls whether the "Since" field from the current
view should be passed down to the slice containing the annotation data.`}
value={!!Object.keys(overrides).find(x => x === 'since')}
onChange={(v) => {
delete overrides.since;
if (v) {
this.setState({ overrides: { ...overrides, since: null } });
} else {
this.setState({ overrides: { ...overrides } });
}
}}
/>
<CheckboxControl
hovered
name="annotation-override-until"
label="Override 'Until'"
description={`This controls whether the "Until" field from the current
view should be passed down to the slice containing the annotation data.`}
value={!!Object.keys(overrides).find(x => x === 'until')}
onChange={(v) => {
delete overrides.until;
if (v) {
this.setState({ overrides: { ...overrides, until: null } });
} else {
this.setState({ overrides: { ...overrides } });
}
}}
/>
<TextControl
hovered
name="annotation-layer-timeshift"
label="Time Shift"
description={`Time delta in natural language
(example: 24 hours, 7 days, 56 weeks, 365 days)`}
placeholder=""
value={overrides.time_shift}
onChange={v => this.setState({ overrides: { ...overrides, time_shift: v } })}
/>
</div>
</PopoverSection>
</div>
);
}
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 (
<PopoverSection
isSelected
onSelect={() => {}}
title="Display configuration"
info="Configure your how you overlay is displayed here."
>
<SelectControl
name="annotation-layer-stroke"
label="Style"
// see '../../../../visualizations/nvd3_vis.css'
options={[
{ value: 'solid', label: 'Solid' },
{ value: 'dashed', label: 'Dashed' },
{ value: 'longDashed', label: 'Long Dashed' },
{ value: 'dotted', label: 'Dotted' },
]}
value={style}
onChange={v => this.setState({ style: v })}
/>
<SelectControl
name="annotation-layer-opacity"
label="Opacity"
// see '../../../../visualizations/nvd3_vis.css'
options={[
{ value: '', label: 'Solid' },
{ value: 'opacityLow', label: '0.2' },
{ value: 'opacityMedium', label: '0.5' },
{ value: 'opacityHigh', label: '0.8' },
]}
value={opacity}
onChange={v => this.setState({ opacity: v })}
/>
<div>
<ControlHeader label="Color" />
<div style={{ display: 'flex', flexDirection: 'column' }}>
<CompactPicker
color={color}
colors={colorScheme}
onChangeComplete={v => this.setState({ color: v.hex })}
/>
<Button
style={{ marginTop: '0.5rem', marginBottom: '0.5rem' }}
bsStyle={color === AUTOMATIC_COLOR ? 'success' : 'default'}
bsSize="xsmall"
onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
>
Automatic Color
</Button>
</div>
</div>
<TextControl
name="annotation-layer-stroke-width"
label="Line Width"
isInt
value={width}
onChange={v => this.setState({ width: v })}
/>
</PopoverSection>
);
}
render() {
const { isNew, name, annotationType,
sourceType, show } = this.state;
const isValid = this.isValidForm();
return (
<div>
{
this.props.error &&
<span style={{ color: 'red' }}>
ERROR: {this.props.error}
</span>
}
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ marginRight: '2rem' }}>
<PopoverSection
isSelected
onSelect={() => {}}
title="Layer Configuration"
info="Configure the basics of your Annotation Layer."
>
<TextControl
name="annotation-layer-name"
label="Name"
placeholder=""
value={name}
onChange={v => this.setState({ name: v })}
validationErrors={!name ? ['Mandatory'] : []}
/>
<CheckboxControl
name="annotation-layer-hide"
label="Hide Layer"
value={!show}
onChange={v => this.setState({ show: !v })}
/>
<SelectControl
hovered
description="Choose the Annotation Layer Type"
label="Annotation Layer Type"
name="annotation-layer-type"
options={getSupportedAnnotationTypes(this.props.vizType).map(
x => ({ value: x, label: getAnnotationTypeLabel(x) }))}
value={annotationType}
onChange={this.handleAnnotationType}
/>
{!!getSupportedSourceTypes(annotationType).length &&
<SelectControl
hovered
description="Choose the source of your annotations"
label="Annotation Source"
name="annotation-source-type"
options={getSupportedSourceTypes(annotationType).map(
x => ({ value: x, label: getAnnotationSourceTypeLabels(x) }))}
value={sourceType}
onChange={this.handleAnnotationSourceType}
/>
}
{ this.renderValueConfiguration() }
</PopoverSection>
</div>
{ this.renderSliceConfiguration() }
{ this.renderDisplayConfiguration() }
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
bsSize="sm"
onClick={this.deleteAnnotation}
>
{ !isNew ? 'Remove' : 'Cancel' }
</Button>
<div>
<Button
bsSize="sm"
disabled={!isValid}
onClick={this.applyAnnotation}
>
Apply
</Button>
<Button
bsSize="sm"
disabled={!isValid}
onClick={this.submitAnnotation}
>
OK
</Button>
</div>
</div>
</div>
);
}
}
AnnotationLayer.propTypes = propTypes;
AnnotationLayer.defaultProps = defaultProps;