fabianmenges commented on a change in pull request #3518: Full Annotation 
Framework
URL: 
https://github.com/apache/incubator-superset/pull/3518#discussion_r156262584
 
 

 ##########
 File path: 
superset/assets/javascripts/explore/components/controls/AnnotationLayer.jsx
 ##########
 @@ -0,0 +1,558 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { CirclePicker } 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, { isQueryAnnotationType, supportedSliceTypes }
+  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,
+  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,
+
+  error: PropTypes.string,
+  colorScheme: PropTypes.string,
+
+  addAnnotationLayer: PropTypes.func,
+  removeAnnotationLayer: PropTypes.func,
+  close: PropTypes.func,
+};
+
+const defaultProps = {
+  name: '',
+  annotationType: AnnotationTypes.FORMULA,
+  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);
+    let isNew;
+    const isLoadingOptions = true;
+    if (!this.props.name) {
+      isNew = true;
+    } else {
+      isNew = false;
+    }
+    const { name, annotationType,
+      color, opacity, style, width, value,
+      overrides, show, titleColumn, descriptionColumns,
+      timeColumn, intervalEndColumn } = props;
+    this.state = {
+      // base
+      name,
+      oldName: isNew ? null : name,
+      annotationType,
+      value,
+      overrides,
+      show,
+      // slice
+      titleColumn,
+      descriptionColumns,
+      timeColumn,
+      intervalEndColumn,
+      // display
+      color: color || AUTOMATIC_COLOR,
+      opacity,
+      style,
+      width,
+      // refData
+      isNew,
+      isLoadingOptions,
+      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.handleValue = this.handleValue.bind(this);
+    this.validate = this.validate.bind(this);
+  }
+
+  componentDidMount() {
+    const { annotationType, isLoadingOptions } = this.state;
+    this.fetchOptions(annotationType, isLoadingOptions);
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    if (prevState.annotationType !== this.state.annotationType) {
+      this.fetchOptions(this.state.annotationType, true);
+    }
+  }
+
+  validateFormula(value, annotationType) {
+    if (annotationType === AnnotationTypes.FORMULA) {
+      try {
+        mathjs.parse(value).compile().eval({ x: 0 });
+      } catch (err) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  validate() {
+    const { name, annotationType, value, timeColumn, intervalEndColumn } = 
this.state;
+    const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)];
+    if (annotationType === annotationType.EVENT) {
+      errors.push(nonEmpty(timeColumn));
+    }
+    if (annotationType === AnnotationTypes.INTERVAL) {
+      errors.push(nonEmpty(timeColumn));
+      errors.push(nonEmpty(intervalEndColumn));
+    }
+    errors.push(this.validateFormula(value, annotationType));
+    return errors.filter(x => x).length;
+  }
+
+  handleAnnotationType(annotationType) {
+    this.setState({
+      annotationType,
+      isLoadingOptions: true,
+      validationErrors: {},
+      value: null,
+    });
+  }
+
+  handleValue(value) {
+    this.setState({
+      value,
+      descriptionColumns: null,
+      intervalEndColumn: null,
+      timeColumn: null,
+      titleColumn: null,
+      overrides: {},
+    });
+  }
+
+  fetchOptions(annotationType, isLoadingOptions) {
+    if (isLoadingOptions === true) {
+      if (annotationType === AnnotationTypes.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 (isQueryAnnotationType(annotationType)) {
+        $.ajax({
+          type: 'GET',
+          url: '/superset/user_slices',
+        }).then(data =>
+          this.setState({
+            isLoadingOptions: false,
+            valueOptions: data.filter(x => supportedSliceTypes(annotationType)
+              .find(v => v === x.viz_type))
+              .map(x => ({ value: x.id, label: x.title, slice: x })),
+          }),
+        );
+      }
+    }
+  }
+
+  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, value, valueOptions, isLoadingOptions } = 
this.state;
+    let label = '';
+    let description = '';
+    if (annotationType === AnnotationTypes.NATIVE) {
+      label = 'Annotation Layer';
+      description = 'Select the Annotation Layer you would like to use.';
+    } else if (isQueryAnnotationType(annotationType)) {
+      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: ' +
+        `[${supportedSliceTypes(annotationType).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.';
+    }
+    if (isQueryAnnotationType(annotationType) || annotationType === 
AnnotationTypes.NATIVE) {
+      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.validateFormula(value, annotationType) ? 
['Bad formula.'] : []}
+        />
+      );
+    }
+    return '';
+  }
+
+  renderSliceConfiguration() {
+    const { annotationType, value, valueOptions, overrides, titleColumn,
+      timeColumn, intervalEndColumn, descriptionColumns } = this.state;
+    const slice = (valueOptions.find(x => x.value === value) || {}).slice;
+    if (isQueryAnnotationType(annotationType) && slice) {
+      const columns = (slice.form_data.groupby || []).concat(
+        (slice.form_data.all_columns || [])).map(x => ({ value: x, label: x 
}));
+      const timeColumnOptions = slice.form_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="Pass down '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="Pass down '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 } });
+                  }
+                }}
+              />
+            </div>
+          </PopoverSection>
+        </div>
+      );
+    }
+    return ('');
+  }
+
+  renderDisplayConfiguration() {
+    const { color, opacity, style, width } = this.state;
+    const colorScheme = ALL_COLOR_SCHEMES[this.props.colorScheme].slice();
+    if (color && color !== AUTOMATIC_COLOR &&
+      !colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
+      colorScheme.push(color);
+    }
+    return (
+      <div>
+        <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: 'wideDashed', label: 'Wide 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' }}>
+              <CirclePicker
+                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
+            placeholder=""
+            value={width}
+            onChange={v => this.setState({ width: v })}
+          />
+        </PopoverSection>
+      </div>
+    );
+  }
+
+  render() {
+    const { isNew, name, annotationType, show } = this.state;
+    const isValid = !this.validate();
+    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
 
 Review comment:
   The main work is to unify their datamodel. I'll give that a shot.

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
[email protected]


With regards,
Apache Git Services

Reply via email to