mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
SIP-32: Moving frontend code to the base of the repo (#9098)
* move assets out, get webpack dev working * update docs to reference superset-frontend * draw the rest of the owl * fix docs * fix webpack script * rats * correct docs * fix tox dox
This commit is contained in:
committed by
GitHub
parent
0cf354cc88
commit
2913063924
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
|
||||
import { t } from '@superset-ui/translation';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import adhocFilterType from '../../propTypes/adhocFilterType';
|
||||
import adhocMetricType from '../../propTypes/adhocMetricType';
|
||||
import savedMetricType from '../../propTypes/savedMetricType';
|
||||
import columnType from '../../propTypes/columnType';
|
||||
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from '../../AdhocFilter';
|
||||
import AdhocMetric from '../../AdhocMetric';
|
||||
import { OPERATORS } from '../../constants';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
import AdhocFilterOption from '../AdhocFilterOption';
|
||||
import FilterDefinitionOption from '../FilterDefinitionOption';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.arrayOf(adhocFilterType),
|
||||
datasource: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(columnType),
|
||||
savedMetrics: PropTypes.arrayOf(savedMetricType),
|
||||
formData: PropTypes.shape({
|
||||
metric: PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
||||
metrics: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
name: '',
|
||||
onChange: () => {},
|
||||
columns: [],
|
||||
savedMetrics: [],
|
||||
formData: {},
|
||||
};
|
||||
|
||||
function isDictionaryForAdhocFilter(value) {
|
||||
return value && !(value instanceof AdhocFilter) && value.expressionType;
|
||||
}
|
||||
|
||||
export default class AdhocFilterControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.optionsForSelect = this.optionsForSelect.bind(this);
|
||||
this.onFilterEdit = this.onFilterEdit.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.getMetricExpression = this.getMetricExpression.bind(this);
|
||||
|
||||
const filters = (this.props.value || []).map(filter =>
|
||||
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
|
||||
);
|
||||
|
||||
this.optionRenderer = VirtualizedRendererWrap(option => (
|
||||
<FilterDefinitionOption option={option} />
|
||||
));
|
||||
this.valueRenderer = adhocFilter => (
|
||||
<AdhocFilterOption
|
||||
adhocFilter={adhocFilter}
|
||||
onFilterEdit={this.onFilterEdit}
|
||||
options={this.state.options}
|
||||
datasource={this.props.datasource}
|
||||
/>
|
||||
);
|
||||
this.state = {
|
||||
values: filters,
|
||||
options: this.optionsForSelect(this.props),
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.columns !== nextProps.columns ||
|
||||
this.props.formData !== nextProps.formData
|
||||
) {
|
||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||
}
|
||||
if (this.props.value !== nextProps.value) {
|
||||
this.setState({
|
||||
values: (nextProps.value || []).map(filter =>
|
||||
isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onFilterEdit(changedFilter) {
|
||||
this.props.onChange(
|
||||
this.state.values.map(value => {
|
||||
if (value.filterOptionName === changedFilter.filterOptionName) {
|
||||
return changedFilter;
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onChange(opts) {
|
||||
this.props.onChange(
|
||||
opts
|
||||
.map(option => {
|
||||
if (option.saved_metric_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
this.props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.saved_metric_name
|
||||
: this.getMetricExpression(option.saved_metric_name),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
} else if (option.label) {
|
||||
return new AdhocFilter({
|
||||
expressionType:
|
||||
this.props.datasource.type === 'druid'
|
||||
? EXPRESSION_TYPES.SIMPLE
|
||||
: EXPRESSION_TYPES.SQL,
|
||||
subject:
|
||||
this.props.datasource.type === 'druid'
|
||||
? option.label
|
||||
: new AdhocMetric(option).translateToSql(),
|
||||
operator: OPERATORS['>'],
|
||||
comparator: 0,
|
||||
clause: CLAUSES.HAVING,
|
||||
});
|
||||
} else if (option.column_name) {
|
||||
return new AdhocFilter({
|
||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||
subject: option.column_name,
|
||||
operator: OPERATORS['=='],
|
||||
comparator: '',
|
||||
clause: CLAUSES.WHERE,
|
||||
});
|
||||
} else if (option instanceof AdhocFilter) {
|
||||
return option;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(option => option),
|
||||
);
|
||||
}
|
||||
|
||||
getMetricExpression(savedMetricName) {
|
||||
return this.props.savedMetrics.find(
|
||||
savedMetric => savedMetric.metric_name === savedMetricName,
|
||||
).expression;
|
||||
}
|
||||
|
||||
optionsForSelect(props) {
|
||||
const options = [
|
||||
...props.columns,
|
||||
...[...(props.formData.metrics || []), props.formData.metric].map(
|
||||
metric =>
|
||||
metric &&
|
||||
(typeof metric === 'string'
|
||||
? { saved_metric_name: metric }
|
||||
: new AdhocMetric(metric)),
|
||||
),
|
||||
].filter(option => option);
|
||||
|
||||
return options
|
||||
.reduce((results, option) => {
|
||||
if (option.saved_metric_name) {
|
||||
results.push({
|
||||
...option,
|
||||
filterOptionName: option.saved_metric_name,
|
||||
});
|
||||
} else if (option.column_name) {
|
||||
results.push({
|
||||
...option,
|
||||
filterOptionName: '_col_' + option.column_name,
|
||||
});
|
||||
} else if (option instanceof AdhocMetric) {
|
||||
results.push({
|
||||
...option,
|
||||
filterOptionName: '_adhocmetric_' + option.label,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, [])
|
||||
.sort((a, b) =>
|
||||
(a.saved_metric_name || a.column_name || a.label).localeCompare(
|
||||
b.saved_metric_name || b.column_name || b.label,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="metrics-select">
|
||||
<ControlHeader {...this.props} />
|
||||
<OnPasteSelect
|
||||
multi
|
||||
name={`select-${this.props.name}`}
|
||||
placeholder={t('choose a column or metric')}
|
||||
options={this.state.options}
|
||||
value={this.state.values}
|
||||
labelKey="label"
|
||||
valueKey="filterOptionName"
|
||||
clearable
|
||||
closeOnSelect
|
||||
onChange={this.onChange}
|
||||
optionRenderer={this.optionRenderer}
|
||||
valueRenderer={this.valueRenderer}
|
||||
selectWrap={VirtualizedSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdhocFilterControl.propTypes = propTypes;
|
||||
AdhocFilterControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,749 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CompactPicker } from 'react-color';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import mathjs from 'mathjs';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
import { getCategoricalSchemeRegistry } from '@superset-ui/color';
|
||||
import { getChartMetadataRegistry } from '@superset-ui/chart';
|
||||
|
||||
import SelectControl from './SelectControl';
|
||||
import TextControl from './TextControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
|
||||
import ANNOTATION_TYPES, {
|
||||
ANNOTATION_SOURCE_TYPES,
|
||||
ANNOTATION_TYPES_METADATA,
|
||||
DEFAULT_ANNOTATION_TYPE,
|
||||
requiresQuery,
|
||||
ANNOTATION_SOURCE_TYPES_METADATA,
|
||||
} from '../../../modules/AnnotationTypes';
|
||||
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import { nonEmpty } from '../../validators';
|
||||
import './AnnotationLayer.less';
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
showMarkers,
|
||||
hideLine,
|
||||
value,
|
||||
overrides,
|
||||
show,
|
||||
titleColumn,
|
||||
descriptionColumns,
|
||||
timeColumn,
|
||||
intervalEndColumn,
|
||||
} = props;
|
||||
|
||||
const overridesKeys = Object.keys(overrides);
|
||||
if (overridesKeys.includes('since') || overridesKeys.includes('until')) {
|
||||
overrides.time_range = null;
|
||||
delete overrides.since;
|
||||
delete overrides.until;
|
||||
}
|
||||
|
||||
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,
|
||||
showMarkers,
|
||||
hideLine,
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
getSupportedSourceTypes(annotationType) {
|
||||
// Get vis types that can be source.
|
||||
const sources = getChartMetadataRegistry()
|
||||
.entries()
|
||||
.filter(({ value: chartMetadata }) =>
|
||||
chartMetadata.canBeAnnotationType(annotationType),
|
||||
)
|
||||
.map(({ key, value: chartMetadata }) => ({
|
||||
value: key,
|
||||
label: chartMetadata.name,
|
||||
}));
|
||||
// Prepend native source if applicable
|
||||
if (ANNOTATION_TYPES_METADATA[annotationType].supportNativeSource) {
|
||||
sources.unshift(ANNOTATION_SOURCE_TYPES_METADATA.NATIVE);
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
isValidFormula(value, annotationType) {
|
||||
if (annotationType === ANNOTATION_TYPES.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 === ANNOTATION_TYPES.EVENT) {
|
||||
errors.push(nonEmpty(timeColumn));
|
||||
}
|
||||
if (annotationType === ANNOTATION_TYPES.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: { time_range: null },
|
||||
});
|
||||
}
|
||||
|
||||
fetchOptions(annotationType, sourceType, isLoadingOptions) {
|
||||
if (isLoadingOptions === true) {
|
||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
SupersetClient.get({
|
||||
endpoint: '/annotationlayermodelview/api/read?',
|
||||
}).then(({ json }) => {
|
||||
const layers = json
|
||||
? json.result.map(layer => ({
|
||||
value: layer.id,
|
||||
label: layer.name,
|
||||
}))
|
||||
: [];
|
||||
this.setState({
|
||||
isLoadingOptions: false,
|
||||
valueOptions: layers,
|
||||
});
|
||||
});
|
||||
} else if (requiresQuery(sourceType)) {
|
||||
SupersetClient.get({ endpoint: '/superset/user_slices' }).then(
|
||||
({ json }) => {
|
||||
const registry = getChartMetadataRegistry();
|
||||
this.setState({
|
||||
isLoadingOptions: false,
|
||||
valueOptions: json
|
||||
.filter(x => {
|
||||
const metadata = registry.get(x.viz_type);
|
||||
return (
|
||||
metadata && metadata.canBeAnnotationType(annotationType)
|
||||
);
|
||||
})
|
||||
.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();
|
||||
}
|
||||
|
||||
renderOption(option) {
|
||||
return (
|
||||
<span className="optionWrapper" title={option.label}>
|
||||
{option.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 = label = t('Chart');
|
||||
description = `Use a pre defined Superset Chart as a source for annotations and overlays.
|
||||
your chart must be one of these visualization types:
|
||||
[${this.getSupportedSourceTypes(annotationType)
|
||||
.map(x => x.label)
|
||||
.join(', ')}]`;
|
||||
}
|
||||
} else if (annotationType === ANNOTATION_TYPES.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'] : []}
|
||||
optionRenderer={this.renderOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (annotationType === ANNOTATION_TYPES.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 === ANNOTATION_TYPES.EVENT ||
|
||||
annotationType === ANNOTATION_TYPES.INTERVAL) && (
|
||||
<SelectControl
|
||||
hovered
|
||||
name="annotation-layer-time-column"
|
||||
label={
|
||||
annotationType === ANNOTATION_TYPES.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 === ANNOTATION_TYPES.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 !== ANNOTATION_TYPES.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-time_range"
|
||||
label="Override time range"
|
||||
description={`This controls whether the "time_range" field from the current
|
||||
view should be passed down to the chart containing the annotation data.`}
|
||||
value={!!Object.keys(overrides).find(x => x === 'time_range')}
|
||||
onChange={v => {
|
||||
delete overrides.time_range;
|
||||
if (v) {
|
||||
this.setState({
|
||||
overrides: { ...overrides, time_range: null },
|
||||
});
|
||||
} else {
|
||||
this.setState({ overrides: { ...overrides } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-override-timegrain"
|
||||
label="Override time grain"
|
||||
description={`This controls whether the time grain field from the current
|
||||
view should be passed down to the chart containing the annotation data.`}
|
||||
value={
|
||||
!!Object.keys(overrides).find(x => x === 'time_grain_sqla')
|
||||
}
|
||||
onChange={v => {
|
||||
delete overrides.time_grain_sqla;
|
||||
delete overrides.granularity;
|
||||
if (v) {
|
||||
this.setState({
|
||||
overrides: {
|
||||
...overrides,
|
||||
time_grain_sqla: null,
|
||||
granularity: 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,
|
||||
showMarkers,
|
||||
hideLine,
|
||||
annotationType,
|
||||
} = this.state;
|
||||
const colorScheme = getCategoricalSchemeRegistry()
|
||||
.get(this.props.colorScheme)
|
||||
.colors.concat();
|
||||
if (
|
||||
color &&
|
||||
color !== AUTOMATIC_COLOR &&
|
||||
!colorScheme.find(x => x.toLowerCase() === color.toLowerCase())
|
||||
) {
|
||||
colorScheme.push(color);
|
||||
}
|
||||
return (
|
||||
<PopoverSection
|
||||
isSelected
|
||||
onSelect={() => {}}
|
||||
title={t('Display configuration')}
|
||||
info={t('Configure your how you overlay is displayed here.')}
|
||||
>
|
||||
<SelectControl
|
||||
name="annotation-layer-stroke"
|
||||
label={t('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={t('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={t('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={t('Line Width')}
|
||||
isInt
|
||||
value={width}
|
||||
onChange={v => this.setState({ width: v })}
|
||||
/>
|
||||
{annotationType === ANNOTATION_TYPES.TIME_SERIES && (
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-layer-show-markers"
|
||||
label="Show Markers"
|
||||
description={'Shows or hides markers for the time series'}
|
||||
value={showMarkers}
|
||||
onChange={v => this.setState({ showMarkers: v })}
|
||||
/>
|
||||
)}
|
||||
{annotationType === ANNOTATION_TYPES.TIME_SERIES && (
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-layer-hide-line"
|
||||
label="Hide Line"
|
||||
description={'Hides the Line for the time series'}
|
||||
value={hideLine}
|
||||
onChange={v => this.setState({ hideLine: v })}
|
||||
/>
|
||||
)}
|
||||
</PopoverSection>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isNew, name, annotationType, sourceType, show } = this.state;
|
||||
const isValid = this.isValidForm();
|
||||
|
||||
const metadata = getChartMetadataRegistry().get(this.props.vizType);
|
||||
const supportedAnnotationTypes = metadata
|
||||
? metadata.supportedAnnotationTypes.map(
|
||||
type => ANNOTATION_TYPES_METADATA[type],
|
||||
)
|
||||
: [];
|
||||
const supportedSourceTypes = this.getSupportedSourceTypes(annotationType);
|
||||
|
||||
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={t('Layer Configuration')}
|
||||
info={t('Configure the basics of your Annotation Layer.')}
|
||||
>
|
||||
<TextControl
|
||||
name="annotation-layer-name"
|
||||
label={t('Name')}
|
||||
placeholder=""
|
||||
value={name}
|
||||
onChange={v => this.setState({ name: v })}
|
||||
validationErrors={!name ? [t('Mandatory')] : []}
|
||||
/>
|
||||
<CheckboxControl
|
||||
name="annotation-layer-hide"
|
||||
label={t('Hide Layer')}
|
||||
value={!show}
|
||||
onChange={v => this.setState({ show: !v })}
|
||||
/>
|
||||
<SelectControl
|
||||
hovered
|
||||
description={t('Choose the Annotation Layer Type')}
|
||||
label={t('Annotation Layer Type')}
|
||||
name="annotation-layer-type"
|
||||
options={supportedAnnotationTypes}
|
||||
value={annotationType}
|
||||
onChange={this.handleAnnotationType}
|
||||
/>
|
||||
{!!supportedSourceTypes.length && (
|
||||
<SelectControl
|
||||
hovered
|
||||
description="Choose the source of your annotations"
|
||||
label="Annotation Source"
|
||||
name="annotation-source-type"
|
||||
options={supportedSourceTypes}
|
||||
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 ? t('Remove') : t('Cancel')}
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
bsSize="sm"
|
||||
disabled={!isValid}
|
||||
onClick={this.applyAnnotation}
|
||||
>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bsSize="sm"
|
||||
disabled={!isValid}
|
||||
onClick={this.submitAnnotation}
|
||||
>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AnnotationLayer.propTypes = propTypes;
|
||||
AnnotationLayer.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.optionWrapper {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { getChartKey } from '../../exploreUtils';
|
||||
import { runAnnotationQuery } from '../../../chart/chartAction';
|
||||
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
|
||||
|
||||
import AnnotationLayer from './AnnotationLayer';
|
||||
|
||||
const propTypes = {
|
||||
colorScheme: PropTypes.string.isRequired,
|
||||
annotationError: PropTypes.object,
|
||||
annotationQuery: PropTypes.object,
|
||||
vizType: PropTypes.string,
|
||||
|
||||
validationErrors: PropTypes.array,
|
||||
name: PropTypes.string.isRequired,
|
||||
actions: PropTypes.object,
|
||||
value: PropTypes.arrayOf(PropTypes.object),
|
||||
onChange: PropTypes.func,
|
||||
refreshAnnotationData: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
vizType: '',
|
||||
value: [],
|
||||
annotationError: {},
|
||||
annotationQuery: {},
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
class AnnotationLayerControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.addAnnotationLayer = this.addAnnotationLayer.bind(this);
|
||||
this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { name, annotationError, validationErrors, value } = nextProps;
|
||||
if (Object.keys(annotationError).length && !validationErrors.length) {
|
||||
this.props.actions.setControlValue(
|
||||
name,
|
||||
value,
|
||||
Object.keys(annotationError),
|
||||
);
|
||||
}
|
||||
if (!Object.keys(annotationError).length && validationErrors.length) {
|
||||
this.props.actions.setControlValue(name, value, []);
|
||||
}
|
||||
}
|
||||
|
||||
addAnnotationLayer(annotationLayer) {
|
||||
const annotation = annotationLayer;
|
||||
let annotations = this.props.value.slice();
|
||||
const i = annotations.findIndex(
|
||||
x => x.name === (annotation.oldName || annotation.name),
|
||||
);
|
||||
delete annotation.oldName;
|
||||
if (i > -1) {
|
||||
annotations[i] = annotation;
|
||||
} else {
|
||||
annotations = annotations.concat(annotation);
|
||||
}
|
||||
this.props.refreshAnnotationData(annotation);
|
||||
this.props.onChange(annotations);
|
||||
}
|
||||
|
||||
removeAnnotationLayer(annotation) {
|
||||
const annotations = this.props.value
|
||||
.slice()
|
||||
.filter(x => x.name !== annotation.oldName);
|
||||
this.props.onChange(annotations);
|
||||
}
|
||||
|
||||
renderPopover(parent, annotation, error) {
|
||||
const id = !annotation ? '_new' : annotation.name;
|
||||
return (
|
||||
<Popover
|
||||
style={{ maxWidth: 'none' }}
|
||||
title={
|
||||
annotation ? t('Edit Annotation Layer') : t('Add Annotation Layer')
|
||||
}
|
||||
id={`annotation-pop-${id}`}
|
||||
>
|
||||
<AnnotationLayer
|
||||
{...annotation}
|
||||
error={error}
|
||||
colorScheme={this.props.colorScheme}
|
||||
vizType={this.props.vizType}
|
||||
addAnnotationLayer={this.addAnnotationLayer}
|
||||
removeAnnotationLayer={this.removeAnnotationLayer}
|
||||
close={() => this.refs[parent].hide()}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
renderInfo(anno) {
|
||||
const { annotationError, annotationQuery } = this.props;
|
||||
if (annotationQuery[anno.name]) {
|
||||
return (
|
||||
<i className="fa fa-refresh" style={{ color: 'orange' }} aria-hidden />
|
||||
);
|
||||
}
|
||||
if (annotationError[anno.name]) {
|
||||
return (
|
||||
<InfoTooltipWithTrigger
|
||||
label="validation-errors"
|
||||
bsStyle="danger"
|
||||
tooltip={annotationError[anno.name]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!anno.show) {
|
||||
return <span style={{ color: 'red' }}> Hidden </span>;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const annotations = this.props.value.map((anno, i) => (
|
||||
<OverlayTrigger
|
||||
key={i}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref={`overlay-${i}`}
|
||||
placement="right"
|
||||
overlay={this.renderPopover(
|
||||
`overlay-${i}`,
|
||||
anno,
|
||||
this.props.annotationError[anno.name],
|
||||
)}
|
||||
>
|
||||
<ListGroupItem>
|
||||
<span>{anno.name}</span>
|
||||
<span style={{ float: 'right' }}>{this.renderInfo(anno)}</span>
|
||||
</ListGroupItem>
|
||||
</OverlayTrigger>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<ListGroup>
|
||||
{annotations}
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="overlay-new"
|
||||
placement="right"
|
||||
overlay={this.renderPopover('overlay-new')}
|
||||
>
|
||||
<ListGroupItem>
|
||||
<i className="fa fa-plus" /> {t('Add Annotation Layer')}
|
||||
</ListGroupItem>
|
||||
</OverlayTrigger>
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AnnotationLayerControl.propTypes = propTypes;
|
||||
AnnotationLayerControl.defaultProps = defaultProps;
|
||||
|
||||
// Tried to hook this up through stores/control.jsx instead of using redux
|
||||
// directly, could not figure out how to get access to the color_scheme
|
||||
function mapStateToProps({ charts, explore }) {
|
||||
const chartKey = getChartKey(explore);
|
||||
const chart = charts[chartKey] || charts[0] || {};
|
||||
|
||||
return {
|
||||
colorScheme: (explore.controls || {}).color_scheme.value,
|
||||
annotationError: chart.annotationError,
|
||||
annotationQuery: chart.annotationQuery,
|
||||
vizType: explore.controls.viz_type.value,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
refreshAnnotationData: annotationLayer =>
|
||||
dispatch(runAnnotationQuery(annotationLayer)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(AnnotationLayerControl);
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row, FormGroup, FormControl } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
value: [null, null],
|
||||
};
|
||||
|
||||
export default class BoundsControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
minMax: [
|
||||
props.value[0] === null ? '' : props.value[0],
|
||||
props.value[1] === null ? '' : props.value[1],
|
||||
],
|
||||
};
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onMinChange = this.onMinChange.bind(this);
|
||||
this.onMaxChange = this.onMaxChange.bind(this);
|
||||
}
|
||||
onMinChange(event) {
|
||||
this.setState(
|
||||
{
|
||||
minMax: [event.target.value, this.state.minMax[1]],
|
||||
},
|
||||
this.onChange,
|
||||
);
|
||||
}
|
||||
onMaxChange(event) {
|
||||
this.setState(
|
||||
{
|
||||
minMax: [this.state.minMax[0], event.target.value],
|
||||
},
|
||||
this.onChange,
|
||||
);
|
||||
}
|
||||
onChange() {
|
||||
const mm = this.state.minMax;
|
||||
const errors = [];
|
||||
if (mm[0] && isNaN(mm[0])) {
|
||||
errors.push(t('`Min` value should be numeric or empty'));
|
||||
}
|
||||
if (mm[1] && isNaN(mm[1])) {
|
||||
errors.push(t('`Max` value should be numeric or empty'));
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
this.props.onChange([parseFloat(mm[0]), parseFloat(mm[1])], errors);
|
||||
} else {
|
||||
this.props.onChange([null, null], errors);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<FormGroup bsSize="small">
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder={t('Min')}
|
||||
onChange={this.onMinChange}
|
||||
value={this.state.minMax[0]}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder={t('Max')}
|
||||
onChange={this.onMaxChange}
|
||||
value={this.state.minMax[1]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BoundsControl.propTypes = propTypes;
|
||||
BoundsControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import Checkbox from '../../../components/Checkbox';
|
||||
|
||||
const propTypes = {
|
||||
value: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
value: false,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
const checkboxStyle = { paddingRight: '5px' };
|
||||
|
||||
export default class CheckboxControl extends React.Component {
|
||||
onChange() {
|
||||
this.props.onChange(!this.props.value);
|
||||
}
|
||||
renderCheckbox() {
|
||||
return (
|
||||
<Checkbox
|
||||
onChange={this.onChange.bind(this)}
|
||||
style={checkboxStyle}
|
||||
checked={!!this.props.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
if (this.props.label) {
|
||||
return (
|
||||
<ControlHeader
|
||||
{...this.props}
|
||||
leftNode={this.renderCheckbox()}
|
||||
onClick={this.onChange.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.renderCheckbox();
|
||||
}
|
||||
}
|
||||
CheckboxControl.propTypes = propTypes;
|
||||
CheckboxControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import shortid from 'shortid';
|
||||
import {
|
||||
SortableContainer,
|
||||
SortableHandle,
|
||||
SortableElement,
|
||||
arrayMove,
|
||||
} from 'react-sortable-hoc';
|
||||
|
||||
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import controlMap from './';
|
||||
import './CollectionControl.less';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
addTooltip: PropTypes.string,
|
||||
itemGenerator: PropTypes.func,
|
||||
keyAccessor: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.array]),
|
||||
isFloat: PropTypes.bool,
|
||||
isInt: PropTypes.bool,
|
||||
controlName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
label: null,
|
||||
description: null,
|
||||
onChange: () => {},
|
||||
placeholder: 'Empty collection',
|
||||
itemGenerator: () => ({ key: shortid.generate() }),
|
||||
keyAccessor: o => o.key,
|
||||
value: [],
|
||||
addTooltip: 'Add an item',
|
||||
};
|
||||
const SortableListGroupItem = SortableElement(ListGroupItem);
|
||||
const SortableListGroup = SortableContainer(ListGroup);
|
||||
const SortableDragger = SortableHandle(() => (
|
||||
<i className="fa fa-bars text-primary" style={{ cursor: 'ns-resize' }} />
|
||||
));
|
||||
|
||||
export default class CollectionControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onAdd = this.onAdd.bind(this);
|
||||
}
|
||||
onChange(i, value) {
|
||||
Object.assign(this.props.value[i], value);
|
||||
this.props.onChange(this.props.value);
|
||||
}
|
||||
onAdd() {
|
||||
this.props.onChange(this.props.value.concat([this.props.itemGenerator()]));
|
||||
}
|
||||
onSortEnd({ oldIndex, newIndex }) {
|
||||
this.props.onChange(arrayMove(this.props.value, oldIndex, newIndex));
|
||||
}
|
||||
removeItem(i) {
|
||||
this.props.onChange(this.props.value.filter((o, ix) => i !== ix));
|
||||
}
|
||||
renderList() {
|
||||
if (this.props.value.length === 0) {
|
||||
return <div className="text-muted">{this.props.placeholder}</div>;
|
||||
}
|
||||
const Control = controlMap[this.props.controlName];
|
||||
return (
|
||||
<SortableListGroup
|
||||
useDragHandle
|
||||
lockAxis="y"
|
||||
onSortEnd={this.onSortEnd.bind(this)}
|
||||
>
|
||||
{this.props.value.map((o, i) => (
|
||||
<SortableListGroupItem
|
||||
className="clearfix"
|
||||
key={this.props.keyAccessor(o)}
|
||||
index={i}
|
||||
>
|
||||
<div className="pull-left m-r-5">
|
||||
<SortableDragger />
|
||||
</div>
|
||||
<div className="pull-left">
|
||||
<Control
|
||||
{...this.props}
|
||||
{...o}
|
||||
onChange={this.onChange.bind(this, i)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<InfoTooltipWithTrigger
|
||||
icon="times"
|
||||
label="remove-item"
|
||||
tooltip="remove item"
|
||||
bsStyle="primary"
|
||||
onClick={this.removeItem.bind(this, i)}
|
||||
/>
|
||||
</div>
|
||||
</SortableListGroupItem>
|
||||
))}
|
||||
</SortableListGroup>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="CollectionControl">
|
||||
<ControlHeader {...this.props} />
|
||||
{this.renderList()}
|
||||
<InfoTooltipWithTrigger
|
||||
icon="plus-circle"
|
||||
label="add-item"
|
||||
tooltip={this.props.addTooltip}
|
||||
bsStyle="primary"
|
||||
className="fa-lg"
|
||||
onClick={this.onAdd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionControl.propTypes = propTypes;
|
||||
CollectionControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
.CollectionControl .list-group-item i.fa {
|
||||
padding-top: 5px;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CategoricalColorNamespace } from '@superset-ui/color';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
colorScheme: PropTypes.string,
|
||||
colorNamespace: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
value: {},
|
||||
colorScheme: undefined,
|
||||
colorNamespace: undefined,
|
||||
};
|
||||
|
||||
export default class ColorMapControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Object.keys(this.props.value).forEach(label => {
|
||||
CategoricalColorNamespace.getScale(
|
||||
this.props.colorScheme,
|
||||
this.props.colorNamespace,
|
||||
).setColor(label, this.props.value[label]);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ColorMapControl.propTypes = propTypes;
|
||||
ColorMapControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import { SketchPicker } from 'react-color';
|
||||
import { getCategoricalSchemeRegistry } from '@superset-ui/color';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
const swatchCommon = {
|
||||
position: 'absolute',
|
||||
width: '50px',
|
||||
height: '20px',
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
};
|
||||
|
||||
const styles = {
|
||||
swatch: {
|
||||
width: '50px',
|
||||
height: '20px',
|
||||
position: 'relative',
|
||||
padding: '5px',
|
||||
borderRadius: '1px',
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
boxShadow:
|
||||
'rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset, rgba(0, 0, 0, 0.25) 0px 0px 4px inset',
|
||||
},
|
||||
color: {
|
||||
...swatchCommon,
|
||||
borderRadius: '2px',
|
||||
},
|
||||
checkboard: {
|
||||
...swatchCommon,
|
||||
background:
|
||||
'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") left center',
|
||||
},
|
||||
};
|
||||
export default class ColorPickerControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange(col) {
|
||||
this.props.onChange(col.rgb);
|
||||
}
|
||||
renderPopover() {
|
||||
const presetColors = getCategoricalSchemeRegistry()
|
||||
.get()
|
||||
.colors.filter((s, i) => i < 7);
|
||||
return (
|
||||
<Popover id="filter-popover" className="color-popover">
|
||||
<SketchPicker
|
||||
color={this.props.value}
|
||||
onChange={this.onChange}
|
||||
presetColors={presetColors}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const c = this.props.value || { r: 0, g: 0, b: 0, a: 0 };
|
||||
const colStyle = Object.assign({}, styles.color, {
|
||||
background: `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<div style={styles.swatch}>
|
||||
<div style={styles.checkboard} />
|
||||
<div style={colStyle} />
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ColorPickerControl.propTypes = propTypes;
|
||||
ColorPickerControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isFunction } from 'lodash';
|
||||
import { Creatable } from 'react-select';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import TooltipWrapper from '../../../components/TooltipWrapper';
|
||||
|
||||
const propTypes = {
|
||||
description: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
default: PropTypes.string,
|
||||
choices: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.array),
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
schemes: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||
isLinear: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
choices: [],
|
||||
schemes: {},
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
export default class ColorSchemeControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
scheme: this.props.value,
|
||||
};
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.renderOption = this.renderOption.bind(this);
|
||||
}
|
||||
|
||||
onChange(option) {
|
||||
const optionValue = option ? option.value : null;
|
||||
this.props.onChange(optionValue);
|
||||
this.setState({ scheme: optionValue });
|
||||
}
|
||||
|
||||
renderOption(key) {
|
||||
const { isLinear, schemes } = this.props;
|
||||
const schemeLookup = isFunction(schemes) ? schemes() : schemes;
|
||||
const currentScheme = schemeLookup[key.value || defaultProps.value];
|
||||
|
||||
// For categorical scheme, display all the colors
|
||||
// For sequential scheme, show 10 or interpolate to 10.
|
||||
// Sequential schemes usually have at most 10 colors.
|
||||
let colors = [];
|
||||
if (currentScheme) {
|
||||
colors = isLinear ? currentScheme.getColors(10) : currentScheme.colors;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipWrapper
|
||||
label={`${currentScheme.id}-tooltip`}
|
||||
tooltip={currentScheme.label}
|
||||
>
|
||||
<ul className="color-scheme-container">
|
||||
{colors.map((color, i) => (
|
||||
<li
|
||||
key={`${currentScheme.id}-${i}`}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
border: `1px solid ${color === 'white' ? 'black' : color}`,
|
||||
}}
|
||||
>
|
||||
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { choices } = this.props;
|
||||
const options = (isFunction(choices) ? choices() : choices).map(choice => ({
|
||||
value: choice[0],
|
||||
label: choice[1],
|
||||
}));
|
||||
|
||||
const selectProps = {
|
||||
multi: false,
|
||||
name: `select-${this.props.name}`,
|
||||
placeholder: `Select (${options.length})`,
|
||||
default: this.props.default,
|
||||
options,
|
||||
value: this.props.value,
|
||||
autosize: false,
|
||||
clearable: false,
|
||||
onChange: this.onChange,
|
||||
optionRenderer: this.renderOption,
|
||||
valueRenderer: this.renderOption,
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<Creatable {...selectProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ColorSchemeControl.propTypes = propTypes;
|
||||
ColorSchemeControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Col,
|
||||
Collapse,
|
||||
DropdownButton,
|
||||
Label,
|
||||
MenuItem,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
Well,
|
||||
} from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import ColumnOption from '../../../components/ColumnOption';
|
||||
import MetricOption from '../../../components/MetricOption';
|
||||
import DatasourceModal from '../../../datasource/DatasourceModal';
|
||||
import ChangeDatasourceModal from '../../../datasource/ChangeDatasourceModal';
|
||||
import TooltipWrapper from '../../../components/TooltipWrapper';
|
||||
import './DatasourceControl.less';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
onDatasourceSave: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
onDatasourceSave: () => {},
|
||||
value: null,
|
||||
};
|
||||
|
||||
class DatasourceControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showEditDatasourceModal: false,
|
||||
showChangeDatasourceModal: false,
|
||||
menuExpanded: false,
|
||||
};
|
||||
this.toggleChangeDatasourceModal = this.toggleChangeDatasourceModal.bind(
|
||||
this,
|
||||
);
|
||||
this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this);
|
||||
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
||||
this.renderDatasource = this.renderDatasource.bind(this);
|
||||
}
|
||||
|
||||
toggleShowDatasource() {
|
||||
this.setState(({ showDatasource }) => ({
|
||||
showDatasource: !showDatasource,
|
||||
}));
|
||||
}
|
||||
|
||||
toggleChangeDatasourceModal() {
|
||||
this.setState(({ showChangeDatasourceModal }) => ({
|
||||
showChangeDatasourceModal: !showChangeDatasourceModal,
|
||||
}));
|
||||
}
|
||||
|
||||
toggleEditDatasourceModal() {
|
||||
this.setState(({ showEditDatasourceModal }) => ({
|
||||
showEditDatasourceModal: !showEditDatasourceModal,
|
||||
}));
|
||||
}
|
||||
|
||||
renderDatasource() {
|
||||
const datasource = this.props.datasource;
|
||||
return (
|
||||
<div className="m-t-10">
|
||||
<Well className="m-t-0">
|
||||
<div className="m-b-10">
|
||||
<Label>
|
||||
<i className="fa fa-database" /> {datasource.database.backend}
|
||||
</Label>
|
||||
{` ${datasource.database.name} `}
|
||||
</div>
|
||||
<Row className="datasource-container">
|
||||
<Col md={6}>
|
||||
<strong>Columns</strong>
|
||||
{datasource.columns.map(col => (
|
||||
<div key={col.column_name}>
|
||||
<ColumnOption showType column={col} />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<strong>Metrics</strong>
|
||||
{datasource.metrics.map(m => (
|
||||
<div key={m.metric_name}>
|
||||
<MetricOption metric={m} showType />
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
</Well>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showChangeDatasourceModal, showEditDatasourceModal } = this.state;
|
||||
const { datasource, onChange, onDatasourceSave, value } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<div className="btn-group label-dropdown">
|
||||
<TooltipWrapper
|
||||
label="change-datasource"
|
||||
tooltip={t('Click to change the datasource')}
|
||||
>
|
||||
<DropdownButton
|
||||
title={datasource.name}
|
||||
className="label label-default label-btn m-r-5"
|
||||
bsSize="sm"
|
||||
id="datasource_menu"
|
||||
>
|
||||
<MenuItem eventKey="3" onClick={this.toggleChangeDatasourceModal}>
|
||||
{t('Change Datasource')}
|
||||
</MenuItem>
|
||||
{datasource.type === 'table' && (
|
||||
<MenuItem
|
||||
eventKey="3"
|
||||
href={`/superset/sqllab?datasourceKey=${value}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('Explore in SQL Lab')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem eventKey="3" onClick={this.toggleEditDatasourceModal}>
|
||||
{t('Edit Datasource')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</TooltipWrapper>
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id={'toggle-datasource-tooltip'}>
|
||||
{t('Expand/collapse datasource configuration')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<a href="#">
|
||||
<i
|
||||
className={`fa fa-${
|
||||
this.state.showDatasource ? 'minus' : 'plus'
|
||||
}-square m-r-5 m-l-5 m-t-4`}
|
||||
onClick={this.toggleShowDatasource}
|
||||
/>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<Collapse in={this.state.showDatasource}>
|
||||
{this.renderDatasource()}
|
||||
</Collapse>
|
||||
<DatasourceModal
|
||||
datasource={datasource}
|
||||
show={showEditDatasourceModal}
|
||||
onDatasourceSave={onDatasourceSave}
|
||||
onHide={this.toggleEditDatasourceModal}
|
||||
/>
|
||||
<ChangeDatasourceModal
|
||||
onDatasourceSave={onDatasourceSave}
|
||||
onHide={this.toggleChangeDatasourceModal}
|
||||
show={showChangeDatasourceModal}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DatasourceControl.propTypes = propTypes;
|
||||
DatasourceControl.defaultProps = defaultProps;
|
||||
|
||||
export default DatasourceControl;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 '../../../../stylesheets/less/variables.less';
|
||||
|
||||
#datasource_menu {
|
||||
border-radius: @border-radius-normal;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
#datasource_menu .caret {
|
||||
position: relative;
|
||||
padding-right: 8px;
|
||||
margin-left: 4px;
|
||||
color: @lightest;
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
#datasource_menu + ul {
|
||||
margin-top: 26px;
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
DropdownButton,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Glyphicon,
|
||||
InputGroup,
|
||||
Label,
|
||||
MenuItem,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
Radio,
|
||||
Tab,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap';
|
||||
import Datetime from 'react-datetime';
|
||||
import 'react-datetime/css/react-datetime.css';
|
||||
import moment from 'moment';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import './DateFilterControl.less';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
|
||||
const TYPES = Object.freeze({
|
||||
DEFAULTS: 'defaults',
|
||||
CUSTOM_START_END: 'custom_start_end',
|
||||
CUSTOM_RANGE: 'custom_range',
|
||||
});
|
||||
const TABS = Object.freeze({
|
||||
DEFAULTS: 'defaults',
|
||||
CUSTOM: 'custom',
|
||||
});
|
||||
const RELATIVE_TIME_OPTIONS = Object.freeze({
|
||||
LAST: 'Last',
|
||||
NEXT: 'Next',
|
||||
});
|
||||
const COMMON_TIME_FRAMES = [
|
||||
'Last day',
|
||||
'Last week',
|
||||
'Last month',
|
||||
'Last quarter',
|
||||
'Last year',
|
||||
'No filter',
|
||||
];
|
||||
const TIME_GRAIN_OPTIONS = [
|
||||
'seconds',
|
||||
'minutes',
|
||||
'hours',
|
||||
'days',
|
||||
'weeks',
|
||||
'months',
|
||||
'years',
|
||||
];
|
||||
|
||||
const MOMENT_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss';
|
||||
const DEFAULT_SINCE = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.subtract(7, 'days')
|
||||
.format(MOMENT_FORMAT);
|
||||
const DEFAULT_UNTIL = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.format(MOMENT_FORMAT);
|
||||
const SEPARATOR = ' : ';
|
||||
const FREEFORM_TOOLTIP = t(
|
||||
'Superset supports smart date parsing. Strings like `last sunday` or ' +
|
||||
'`last october` can be used.',
|
||||
);
|
||||
|
||||
const DATE_FILTER_POPOVER_STYLE = { width: '250px' };
|
||||
|
||||
const propTypes = {
|
||||
animation: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
height: PropTypes.number,
|
||||
onOpenDateFilterControl: PropTypes.func,
|
||||
onCloseDateFilterControl: PropTypes.func,
|
||||
endpoints: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
animation: true,
|
||||
onChange: () => {},
|
||||
value: 'Last week',
|
||||
onOpenDateFilterControl: () => {},
|
||||
onCloseDateFilterControl: () => {},
|
||||
};
|
||||
|
||||
function isValidMoment(s) {
|
||||
/* Moment sometimes consider invalid dates as valid, eg, "10 years ago" gets
|
||||
* parsed as "Fri Jan 01 2010 00:00:00" local time. This function does a
|
||||
* better check by comparing a string with a parse/format roundtrip.
|
||||
*/
|
||||
return s === moment(s, MOMENT_FORMAT).format(MOMENT_FORMAT);
|
||||
}
|
||||
|
||||
function getStateFromSeparator(value) {
|
||||
const [since, until] = value.split(SEPARATOR, 2);
|
||||
return { since, until, type: TYPES.CUSTOM_START_END, tab: TABS.CUSTOM };
|
||||
}
|
||||
|
||||
function getStateFromCommonTimeFrame(value) {
|
||||
const units = value.split(' ')[1] + 's';
|
||||
return {
|
||||
tab: TABS.DEFAULTS,
|
||||
type: TYPES.DEFAULTS,
|
||||
common: value,
|
||||
since: moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.subtract(1, units)
|
||||
.format(MOMENT_FORMAT),
|
||||
until: moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.format(MOMENT_FORMAT),
|
||||
};
|
||||
}
|
||||
|
||||
function getStateFromCustomRange(value) {
|
||||
const [rel, num, grain] = value.split(' ', 3);
|
||||
let since;
|
||||
let until;
|
||||
if (rel === RELATIVE_TIME_OPTIONS.LAST) {
|
||||
until = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.format(MOMENT_FORMAT);
|
||||
since = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.subtract(num, grain)
|
||||
.format(MOMENT_FORMAT);
|
||||
} else {
|
||||
until = moment()
|
||||
.utc()
|
||||
.startOf('day')
|
||||
.add(num, grain)
|
||||
.format(MOMENT_FORMAT);
|
||||
since = moment()
|
||||
.startOf('day')
|
||||
.format(MOMENT_FORMAT);
|
||||
}
|
||||
return {
|
||||
tab: TABS.CUSTOM,
|
||||
type: TYPES.CUSTOM_RANGE,
|
||||
common: null,
|
||||
rel,
|
||||
num,
|
||||
grain,
|
||||
since,
|
||||
until,
|
||||
};
|
||||
}
|
||||
|
||||
export default class DateFilterControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
type: TYPES.DEFAULTS,
|
||||
tab: TABS.DEFAULTS,
|
||||
|
||||
// default time frames, for convenience
|
||||
common: COMMON_TIME_FRAMES[0],
|
||||
|
||||
// "last 7 days", "next 4 weeks", etc.
|
||||
rel: RELATIVE_TIME_OPTIONS.LAST,
|
||||
num: '7',
|
||||
grain: TIME_GRAIN_OPTIONS[3],
|
||||
|
||||
// distinct start/end values, either ISO or freeform
|
||||
since: DEFAULT_SINCE,
|
||||
until: DEFAULT_UNTIL,
|
||||
|
||||
// react-datetime has a `closeOnSelect` prop, but it's buggy... so we
|
||||
// handle the calendar visibility here ourselves
|
||||
showSinceCalendar: false,
|
||||
showUntilCalendar: false,
|
||||
sinceViewMode: 'days',
|
||||
untilViewMode: 'days',
|
||||
};
|
||||
|
||||
const value = props.value;
|
||||
if (value.indexOf(SEPARATOR) >= 0) {
|
||||
this.state = { ...this.state, ...getStateFromSeparator(value) };
|
||||
} else if (COMMON_TIME_FRAMES.indexOf(value) >= 0) {
|
||||
this.state = { ...this.state, ...getStateFromCommonTimeFrame(value) };
|
||||
} else {
|
||||
this.state = { ...this.state, ...getStateFromCustomRange(value) };
|
||||
}
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleClickTrigger = this.handleClickTrigger.bind(this);
|
||||
this.isValidSince = this.isValidSince.bind(this);
|
||||
this.isValidUntil = this.isValidUntil.bind(this);
|
||||
this.onEnter = this.onEnter.bind(this);
|
||||
this.renderInput = this.renderInput.bind(this);
|
||||
this.setCustomRange = this.setCustomRange.bind(this);
|
||||
this.setCustomStartEnd = this.setCustomStartEnd.bind(this);
|
||||
this.setTypeCustomRange = this.setTypeCustomRange.bind(this);
|
||||
this.setTypeCustomStartEnd = this.setTypeCustomStartEnd.bind(this);
|
||||
this.toggleCalendar = this.toggleCalendar.bind(this);
|
||||
this.changeTab = this.changeTab.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleClick);
|
||||
}
|
||||
onEnter(event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
setCustomRange(key, value) {
|
||||
const updatedState = { ...this.state, [key]: value };
|
||||
const combinedValue = [
|
||||
updatedState.rel,
|
||||
updatedState.num,
|
||||
updatedState.grain,
|
||||
].join(' ');
|
||||
this.setState(getStateFromCustomRange(combinedValue));
|
||||
}
|
||||
setCustomStartEnd(key, value) {
|
||||
const closeCalendar =
|
||||
(key === 'since' && this.state.sinceViewMode === 'days') ||
|
||||
(key === 'until' && this.state.untilViewMode === 'days');
|
||||
this.setState({
|
||||
type: TYPES.CUSTOM_START_END,
|
||||
[key]: typeof value === 'string' ? value : value.format(MOMENT_FORMAT),
|
||||
showSinceCalendar: this.state.showSinceCalendar && !closeCalendar,
|
||||
showUntilCalendar: this.state.showUntilCalendar && !closeCalendar,
|
||||
sinceViewMode: closeCalendar ? 'days' : this.state.sinceViewMode,
|
||||
untilViewMode: closeCalendar ? 'days' : this.state.untilViewMode,
|
||||
});
|
||||
}
|
||||
setTypeCustomRange() {
|
||||
this.setState({ type: TYPES.CUSTOM_RANGE });
|
||||
}
|
||||
setTypeCustomStartEnd() {
|
||||
this.setState({ type: TYPES.CUSTOM_START_END });
|
||||
}
|
||||
|
||||
changeTab() {
|
||||
const { tab } = this.state;
|
||||
if (tab === TABS.CUSTOM) {
|
||||
this.setState({ tab: TABS.DEFAULTS });
|
||||
} else if (tab === TABS.DEFAULTS) {
|
||||
this.setState({ tab: TABS.CUSTOM });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
const target = e.target;
|
||||
// switch to `TYPES.CUSTOM_START_END` when the calendar is clicked
|
||||
if (this.startEndSectionRef && this.startEndSectionRef.contains(target)) {
|
||||
this.setTypeCustomStartEnd();
|
||||
}
|
||||
|
||||
// if user click outside popover, popover will hide and we will call onCloseDateFilterControl,
|
||||
// but need to exclude OverlayTrigger component to avoid handle click events twice.
|
||||
if (target.getAttribute('name') !== 'popover-trigger') {
|
||||
if (this.popoverContainer && !this.popoverContainer.contains(target)) {
|
||||
this.props.onCloseDateFilterControl();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClickTrigger() {
|
||||
// when user clicks OverlayTrigger,
|
||||
// popoverContainer component will be created after handleClickTrigger
|
||||
// and before handleClick handler
|
||||
if (!this.popoverContainer) {
|
||||
this.props.onOpenDateFilterControl();
|
||||
} else {
|
||||
this.props.onCloseDateFilterControl();
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
let val;
|
||||
if (
|
||||
this.state.type === TYPES.DEFAULTS ||
|
||||
this.state.tab === TABS.DEFAULTS
|
||||
) {
|
||||
val = this.state.common;
|
||||
} else if (this.state.type === TYPES.CUSTOM_RANGE) {
|
||||
val = `${this.state.rel} ${this.state.num} ${this.state.grain}`;
|
||||
} else {
|
||||
val = [this.state.since, this.state.until].join(SEPARATOR);
|
||||
}
|
||||
this.props.onCloseDateFilterControl();
|
||||
this.props.onChange(val);
|
||||
this.refs.trigger.hide();
|
||||
this.setState({ showSinceCalendar: false, showUntilCalendar: false });
|
||||
}
|
||||
isValidSince(date) {
|
||||
return (
|
||||
!isValidMoment(this.state.until) ||
|
||||
date <= moment(this.state.until, MOMENT_FORMAT)
|
||||
);
|
||||
}
|
||||
isValidUntil(date) {
|
||||
return (
|
||||
!isValidMoment(this.state.since) ||
|
||||
date >= moment(this.state.since, MOMENT_FORMAT)
|
||||
);
|
||||
}
|
||||
toggleCalendar(key) {
|
||||
const nextState = {};
|
||||
if (key === 'showSinceCalendar') {
|
||||
nextState.showSinceCalendar = !this.state.showSinceCalendar;
|
||||
if (!this.state.showSinceCalendar) {
|
||||
nextState.showUntilCalendar = false;
|
||||
}
|
||||
} else if (key === 'showUntilCalendar') {
|
||||
nextState.showUntilCalendar = !this.state.showUntilCalendar;
|
||||
if (!this.state.showUntilCalendar) {
|
||||
nextState.showSinceCalendar = false;
|
||||
}
|
||||
}
|
||||
this.setState(nextState);
|
||||
}
|
||||
renderInput(props, key) {
|
||||
return (
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
{...props}
|
||||
type="text"
|
||||
onKeyPress={this.onEnter}
|
||||
onFocus={this.setTypeCustomStartEnd}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<InputGroup.Button onClick={() => this.toggleCalendar(key)}>
|
||||
<Button>
|
||||
<Glyphicon glyph="calendar" style={{ padding: 3 }} />
|
||||
</Button>
|
||||
</InputGroup.Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
const grainOptions = TIME_GRAIN_OPTIONS.map(grain => (
|
||||
<MenuItem
|
||||
onSelect={value => this.setCustomRange('grain', value)}
|
||||
key={grain}
|
||||
eventKey={grain}
|
||||
active={grain === this.state.grain}
|
||||
>
|
||||
{grain}
|
||||
</MenuItem>
|
||||
));
|
||||
const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => {
|
||||
const nextState = getStateFromCommonTimeFrame(timeFrame);
|
||||
const endpoints = this.props.endpoints;
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={timeFrame}
|
||||
placement="left"
|
||||
overlay={
|
||||
<Tooltip id={`tooltip-${timeFrame}`}>
|
||||
{nextState.since} {endpoints && `(${endpoints[0]})`}
|
||||
<br />
|
||||
{nextState.until} {endpoints && `(${endpoints[1]})`}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Radio
|
||||
key={timeFrame.replace(' ', '').toLowerCase()}
|
||||
checked={this.state.common === timeFrame}
|
||||
onChange={() => this.setState(nextState)}
|
||||
>
|
||||
{timeFrame}
|
||||
</Radio>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Popover id="filter-popover" placement="top" positionTop={0}>
|
||||
<div
|
||||
style={DATE_FILTER_POPOVER_STYLE}
|
||||
ref={ref => {
|
||||
this.popoverContainer = ref;
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
defaultActiveKey={this.state.tab === TABS.DEFAULTS ? 1 : 2}
|
||||
id="type"
|
||||
className="time-filter-tabs"
|
||||
onSelect={this.changeTab}
|
||||
>
|
||||
<Tab eventKey={1} title="Defaults">
|
||||
<FormGroup>{timeFrames}</FormGroup>
|
||||
</Tab>
|
||||
<Tab eventKey={2} title="Custom">
|
||||
<FormGroup>
|
||||
<PopoverSection
|
||||
title="Relative to today"
|
||||
isSelected={this.state.type === TYPES.CUSTOM_RANGE}
|
||||
onSelect={this.setTypeCustomRange}
|
||||
>
|
||||
<div
|
||||
className="clearfix centered"
|
||||
style={{ marginTop: '12px' }}
|
||||
>
|
||||
<div
|
||||
style={{ width: '60px', marginTop: '-4px' }}
|
||||
className="input-inline"
|
||||
>
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
componentClass={InputGroup.Button}
|
||||
id="input-dropdown-rel"
|
||||
title={this.state.rel}
|
||||
onFocus={this.setTypeCustomRange}
|
||||
>
|
||||
<MenuItem
|
||||
onSelect={value => this.setCustomRange('rel', value)}
|
||||
key={RELATIVE_TIME_OPTIONS.LAST}
|
||||
eventKey={RELATIVE_TIME_OPTIONS.LAST}
|
||||
active={this.state.rel === RELATIVE_TIME_OPTIONS.LAST}
|
||||
>
|
||||
Last
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelect={value => this.setCustomRange('rel', value)}
|
||||
key={RELATIVE_TIME_OPTIONS.NEXT}
|
||||
eventKey={RELATIVE_TIME_OPTIONS.NEXT}
|
||||
active={this.state.rel === RELATIVE_TIME_OPTIONS.NEXT}
|
||||
>
|
||||
Next
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: '60px', marginTop: '-4px' }}
|
||||
className="input-inline"
|
||||
>
|
||||
<FormControl
|
||||
bsSize="small"
|
||||
type="text"
|
||||
onChange={event =>
|
||||
this.setCustomRange('num', event.target.value)
|
||||
}
|
||||
onFocus={this.setTypeCustomRange}
|
||||
onKeyPress={this.onEnter}
|
||||
value={this.state.num}
|
||||
style={{ height: '30px' }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: '90px', marginTop: '-4px' }}
|
||||
className="input-inline"
|
||||
>
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
componentClass={InputGroup.Button}
|
||||
id="input-dropdown-grain"
|
||||
title={this.state.grain}
|
||||
onFocus={this.setTypeCustomRange}
|
||||
>
|
||||
{grainOptions}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverSection>
|
||||
<PopoverSection
|
||||
title="Start / end"
|
||||
isSelected={this.state.type === TYPES.CUSTOM_START_END}
|
||||
onSelect={this.setTypeCustomStartEnd}
|
||||
info={FREEFORM_TOOLTIP}
|
||||
>
|
||||
<div
|
||||
ref={ref => {
|
||||
this.startEndSectionRef = ref;
|
||||
}}
|
||||
>
|
||||
<InputGroup>
|
||||
<div style={{ margin: '5px 0' }}>
|
||||
<Datetime
|
||||
value={this.state.since}
|
||||
defaultValue={this.state.since}
|
||||
viewDate={this.state.since}
|
||||
onChange={value =>
|
||||
this.setCustomStartEnd('since', value)
|
||||
}
|
||||
isValidDate={this.isValidSince}
|
||||
onClick={this.setTypeCustomStartEnd}
|
||||
renderInput={props =>
|
||||
this.renderInput(props, 'showSinceCalendar')
|
||||
}
|
||||
open={this.state.showSinceCalendar}
|
||||
viewMode={this.state.sinceViewMode}
|
||||
onViewModeChange={sinceViewMode =>
|
||||
this.setState({ sinceViewMode })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ margin: '5px 0' }}>
|
||||
<Datetime
|
||||
value={this.state.until}
|
||||
defaultValue={this.state.until}
|
||||
viewDate={this.state.until}
|
||||
onChange={value =>
|
||||
this.setCustomStartEnd('until', value)
|
||||
}
|
||||
isValidDate={this.isValidUntil}
|
||||
onClick={this.setTypeCustomStartEnd}
|
||||
renderInput={props =>
|
||||
this.renderInput(props, 'showUntilCalendar')
|
||||
}
|
||||
open={this.state.showUntilCalendar}
|
||||
viewMode={this.state.untilViewMode}
|
||||
onViewModeChange={untilViewMode =>
|
||||
this.setState({ untilViewMode })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</PopoverSection>
|
||||
</FormGroup>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div className="clearfix">
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="float-right ok"
|
||||
bsStyle="primary"
|
||||
onClick={this.close}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let value = this.props.value || defaultProps.value;
|
||||
const endpoints = this.props.endpoints;
|
||||
value = value
|
||||
.split(SEPARATOR)
|
||||
.map(
|
||||
(v, idx, values) =>
|
||||
(v.replace('T00:00:00', '') || (idx === 0 ? '-∞' : '∞')) +
|
||||
(endpoints && values.length > 1 ? ` (${endpoints[idx]})` : ''),
|
||||
)
|
||||
.join(SEPARATOR);
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
animation={this.props.animation}
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
onClick={this.handleClickTrigger}
|
||||
>
|
||||
<Label name="popover-trigger" style={{ cursor: 'pointer' }}>
|
||||
{value}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateFilterControl.propTypes = propTypes;
|
||||
DateFilterControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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 '../../../../stylesheets/less/variables.less';
|
||||
|
||||
.rdtPicker table {
|
||||
font-size: @font-size-s;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
|
||||
import FormRow from '../../../components/FormRow';
|
||||
import SelectControl from './SelectControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
import TextControl from './TextControl';
|
||||
|
||||
const propTypes = {
|
||||
datasource: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
asc: PropTypes.bool,
|
||||
clearable: PropTypes.bool,
|
||||
multiple: PropTypes.bool,
|
||||
column: PropTypes.string,
|
||||
metric: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
asc: true,
|
||||
clearable: true,
|
||||
multiple: true,
|
||||
};
|
||||
|
||||
const STYLE_WIDTH = { width: 350 };
|
||||
|
||||
export default class FilterBoxItemControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { column, metric, asc, clearable, multiple, defaultValue } = props;
|
||||
const state = { column, metric, asc, clearable, multiple, defaultValue };
|
||||
this.state = state;
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onControlChange = this.onControlChange.bind(this);
|
||||
}
|
||||
onChange() {
|
||||
this.props.onChange(this.state);
|
||||
}
|
||||
onControlChange(attr, value) {
|
||||
this.setState({ [attr]: value }, this.onChange);
|
||||
}
|
||||
setType() {}
|
||||
textSummary() {
|
||||
return this.state.column || 'N/A';
|
||||
}
|
||||
renderForm() {
|
||||
return (
|
||||
<div>
|
||||
<FormRow
|
||||
label={t('Column')}
|
||||
control={
|
||||
<SelectControl
|
||||
value={this.state.column}
|
||||
name="column"
|
||||
clearable={false}
|
||||
options={this.props.datasource.columns
|
||||
.filter(col => col !== this.state.column)
|
||||
.map(col => ({
|
||||
value: col.column_name,
|
||||
label: col.column_name,
|
||||
}))
|
||||
.concat([
|
||||
{ value: this.state.column, label: this.state.column },
|
||||
])}
|
||||
onChange={v => this.onControlChange('column', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormRow
|
||||
label={t('Label')}
|
||||
control={
|
||||
<TextControl
|
||||
value={this.state.label}
|
||||
name="label"
|
||||
onChange={v => this.onControlChange('label', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormRow
|
||||
label={t('Default')}
|
||||
tooltip={t(
|
||||
'(optional) default value for the filter, when using ' +
|
||||
'the multiple option, you can use a semicolon-delimited list ' +
|
||||
'of options.',
|
||||
)}
|
||||
control={
|
||||
<TextControl
|
||||
value={this.state.defaultValue}
|
||||
name="defaultValue"
|
||||
onChange={v => this.onControlChange('defaultValue', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormRow
|
||||
label={t('Sort Metric')}
|
||||
tooltip={t('Metric to sort the results by')}
|
||||
control={
|
||||
<SelectControl
|
||||
value={this.state.metric}
|
||||
name="column"
|
||||
options={this.props.datasource.metrics
|
||||
.filter(metric => metric !== this.state.metric)
|
||||
.map(m => ({
|
||||
value: m.metric_name,
|
||||
label: m.metric_name,
|
||||
}))
|
||||
.concat([
|
||||
{ value: this.state.metric, label: this.state.metric },
|
||||
])}
|
||||
onChange={v => this.onControlChange('metric', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormRow
|
||||
label={t('Sort Ascending')}
|
||||
tooltip={t('Check for sorting ascending')}
|
||||
isCheckbox
|
||||
control={
|
||||
<CheckboxControl
|
||||
value={this.state.asc}
|
||||
onChange={v => this.onControlChange('asc', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormRow
|
||||
label={t('Allow Multiple Selections')}
|
||||
isCheckbox
|
||||
tooltip={t(
|
||||
'Multiple selections allowed, otherwise filter ' +
|
||||
'is limited to a single value',
|
||||
)}
|
||||
control={
|
||||
<CheckboxControl
|
||||
value={this.state.multiple}
|
||||
onChange={v => this.onControlChange('multiple', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormRow
|
||||
label={t('Required')}
|
||||
tooltip={t('User must select a value for this filter')}
|
||||
isCheckbox
|
||||
control={
|
||||
<CheckboxControl
|
||||
value={!this.state.clearable}
|
||||
onChange={v => this.onControlChange('clearable', !v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
return (
|
||||
<Popover id="ts-col-popo" title={t('Filter Configuration')}>
|
||||
<div style={STYLE_WIDTH}>{this.renderForm()}</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
{this.textSummary()}{' '}
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<InfoTooltipWithTrigger
|
||||
icon="edit"
|
||||
className="text-primary"
|
||||
label="edit-ts-column"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterBoxItemControl.propTypes = propTypes;
|
||||
FilterBoxItemControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, Panel } from 'react-bootstrap';
|
||||
|
||||
import TextControl from './TextControl';
|
||||
import MetricsControl from './MetricsControl';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
|
||||
const controlTypes = {
|
||||
fixed: 'fix',
|
||||
metric: 'metric',
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
isFloat: PropTypes.bool,
|
||||
datasource: PropTypes.object.isRequired,
|
||||
default: PropTypes.shape({
|
||||
type: PropTypes.oneOf(['fix', 'metric']),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
}),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: controlTypes.fixed, value: 5 },
|
||||
};
|
||||
|
||||
export default class FixedOrMetricControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.setType = this.setType.bind(this);
|
||||
this.setFixedValue = this.setFixedValue.bind(this);
|
||||
this.setMetric = this.setMetric.bind(this);
|
||||
this.toggle = this.toggle.bind(this);
|
||||
const type =
|
||||
(props.value ? props.value.type : props.default.type) ||
|
||||
controlTypes.fixed;
|
||||
const value =
|
||||
(props.value ? props.value.value : props.default.value) || '100';
|
||||
this.state = {
|
||||
type,
|
||||
fixedValue: type === controlTypes.fixed ? value : '',
|
||||
metricValue: type === controlTypes.metric ? value : null,
|
||||
};
|
||||
}
|
||||
onChange() {
|
||||
this.props.onChange({
|
||||
type: this.state.type,
|
||||
value:
|
||||
this.state.type === controlTypes.fixed
|
||||
? this.state.fixedValue
|
||||
: this.state.metricValue,
|
||||
});
|
||||
}
|
||||
setType(type) {
|
||||
this.setState({ type }, this.onChange);
|
||||
}
|
||||
setFixedValue(fixedValue) {
|
||||
this.setState({ fixedValue }, this.onChange);
|
||||
}
|
||||
setMetric(metricValue) {
|
||||
this.setState({ metricValue }, this.onChange);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
const expanded = !this.state.expanded;
|
||||
this.setState({
|
||||
expanded,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.props.value || this.props.default;
|
||||
const type = value.type || controlTypes.fixed;
|
||||
const columns = this.props.datasource
|
||||
? this.props.datasource.columns
|
||||
: null;
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<Label style={{ cursor: 'pointer' }} onClick={this.toggle}>
|
||||
{this.state.type === controlTypes.fixed && (
|
||||
<span>{this.state.fixedValue}</span>
|
||||
)}
|
||||
{this.state.type === controlTypes.metric && (
|
||||
<span>
|
||||
<span style={{ fontWeight: 'normal' }}>metric: </span>
|
||||
<strong>
|
||||
{this.state.metricValue ? this.state.metricValue.label : null}
|
||||
</strong>
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Panel
|
||||
className="panel-spreaded"
|
||||
collapsible
|
||||
expanded={this.state.expanded}
|
||||
>
|
||||
<div className="well">
|
||||
<PopoverSection
|
||||
title="Fixed"
|
||||
isSelected={type === controlTypes.fixed}
|
||||
onSelect={() => {
|
||||
this.setType(controlTypes.fixed);
|
||||
}}
|
||||
>
|
||||
<TextControl
|
||||
isFloat
|
||||
onChange={this.setFixedValue}
|
||||
onFocus={() => {
|
||||
this.setType(controlTypes.fixed);
|
||||
}}
|
||||
value={this.state.fixedValue}
|
||||
/>
|
||||
</PopoverSection>
|
||||
<PopoverSection
|
||||
title="Based on a metric"
|
||||
isSelected={type === controlTypes.metric}
|
||||
onSelect={() => {
|
||||
this.setType(controlTypes.metric);
|
||||
}}
|
||||
>
|
||||
<MetricsControl
|
||||
name="metric"
|
||||
columns={columns}
|
||||
multi={false}
|
||||
onFocus={() => {
|
||||
this.setType(controlTypes.metric);
|
||||
}}
|
||||
onChange={this.setMetric}
|
||||
value={this.state.metricValue}
|
||||
/>
|
||||
</PopoverSection>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FixedOrMetricControl.propTypes = propTypes;
|
||||
FixedOrMetricControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormControl } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
export default function HiddenControl(props) {
|
||||
// This wouldn't be necessary but might as well
|
||||
return <FormControl type="hidden" value={props.value} />;
|
||||
}
|
||||
|
||||
HiddenControl.propTypes = propTypes;
|
||||
HiddenControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
import MetricDefinitionOption from '../MetricDefinitionOption';
|
||||
import MetricDefinitionValue from '../MetricDefinitionValue';
|
||||
import AdhocMetric from '../../AdhocMetric';
|
||||
import columnType from '../../propTypes/columnType';
|
||||
import savedMetricType from '../../propTypes/savedMetricType';
|
||||
import adhocMetricType from '../../propTypes/adhocMetricType';
|
||||
import {
|
||||
AGGREGATES,
|
||||
sqlaAutoGeneratedMetricNameRegex,
|
||||
druidAutoGeneratedMetricRegex,
|
||||
} from '../../constants';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, adhocMetricType])),
|
||||
PropTypes.oneOfType([PropTypes.string, adhocMetricType]),
|
||||
]),
|
||||
columns: PropTypes.arrayOf(columnType),
|
||||
savedMetrics: PropTypes.arrayOf(savedMetricType),
|
||||
multi: PropTypes.bool,
|
||||
clearable: PropTypes.bool,
|
||||
datasourceType: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
clearable: true,
|
||||
savedMetrics: [],
|
||||
columns: [],
|
||||
};
|
||||
|
||||
function isDictionaryForAdhocMetric(value) {
|
||||
return value && !(value instanceof AdhocMetric) && value.expressionType;
|
||||
}
|
||||
|
||||
function columnsContainAllMetrics(value, nextProps) {
|
||||
const columnNames = new Set(
|
||||
[...(nextProps.columns || []), ...(nextProps.savedMetrics || [])]
|
||||
// eslint-disable-next-line camelcase
|
||||
.map(({ column_name, metric_name }) => column_name || metric_name),
|
||||
);
|
||||
|
||||
return (
|
||||
(Array.isArray(value) ? value : [value])
|
||||
.filter(metric => metric)
|
||||
// find column names
|
||||
.map(metric =>
|
||||
metric.column
|
||||
? metric.column.column_name
|
||||
: metric.column_name || metric,
|
||||
)
|
||||
.filter(name => name && typeof name === 'string')
|
||||
.every(name => columnNames.has(name))
|
||||
);
|
||||
}
|
||||
|
||||
// adhoc metrics are stored as dictionaries in URL params. We convert them back into the
|
||||
// AdhocMetric class for typechecking, consistency and instance method access.
|
||||
function coerceAdhocMetrics(value) {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
if (isDictionaryForAdhocMetric(value)) {
|
||||
return [new AdhocMetric(value)];
|
||||
}
|
||||
return [value];
|
||||
}
|
||||
return value.map(val => {
|
||||
if (isDictionaryForAdhocMetric(val)) {
|
||||
return new AdhocMetric(val);
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultAggregateForColumn(column) {
|
||||
const type = column.type;
|
||||
if (typeof type !== 'string') {
|
||||
return AGGREGATES.COUNT;
|
||||
} else if (type === '' || type === 'expression') {
|
||||
return AGGREGATES.SUM;
|
||||
} else if (
|
||||
type.match(/.*char.*/i) ||
|
||||
type.match(/string.*/i) ||
|
||||
type.match(/.*text.*/i)
|
||||
) {
|
||||
return AGGREGATES.COUNT_DISTINCT;
|
||||
} else if (
|
||||
type.match(/.*int.*/i) ||
|
||||
type === 'LONG' ||
|
||||
type === 'DOUBLE' ||
|
||||
type === 'FLOAT'
|
||||
) {
|
||||
return AGGREGATES.SUM;
|
||||
} else if (type.match(/.*bool.*/i)) {
|
||||
return AGGREGATES.MAX;
|
||||
} else if (type.match(/.*time.*/i)) {
|
||||
return AGGREGATES.COUNT;
|
||||
} else if (type.match(/unknown/i)) {
|
||||
return AGGREGATES.COUNT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default class MetricsControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onMetricEdit = this.onMetricEdit.bind(this);
|
||||
this.checkIfAggregateInInput = this.checkIfAggregateInInput.bind(this);
|
||||
this.optionsForSelect = this.optionsForSelect.bind(this);
|
||||
this.selectFilterOption = this.selectFilterOption.bind(this);
|
||||
this.isAutoGeneratedMetric = this.isAutoGeneratedMetric.bind(this);
|
||||
this.optionRenderer = VirtualizedRendererWrap(
|
||||
option => <MetricDefinitionOption option={option} />,
|
||||
{ ignoreAutogeneratedMetrics: true },
|
||||
);
|
||||
this.valueRenderer = option => (
|
||||
<MetricDefinitionValue
|
||||
option={option}
|
||||
onMetricEdit={this.onMetricEdit}
|
||||
columns={this.props.columns}
|
||||
multi={this.props.multi}
|
||||
datasourceType={this.props.datasourceType}
|
||||
/>
|
||||
);
|
||||
this.refFunc = ref => {
|
||||
if (ref) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
this.select = ref._selectRef;
|
||||
}
|
||||
};
|
||||
this.state = {
|
||||
aggregateInInput: null,
|
||||
options: this.optionsForSelect(this.props),
|
||||
value: coerceAdhocMetrics(this.props.value),
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { value } = this.props;
|
||||
if (
|
||||
!isEqual(this.props.columns, nextProps.columns) ||
|
||||
!isEqual(this.props.savedMetrics, nextProps.savedMetrics)
|
||||
) {
|
||||
this.setState({ options: this.optionsForSelect(nextProps) });
|
||||
|
||||
// Remove metrics if selected value no longer a column
|
||||
const containsAllMetrics = columnsContainAllMetrics(value, nextProps);
|
||||
|
||||
if (!containsAllMetrics) {
|
||||
this.props.onChange([]);
|
||||
}
|
||||
}
|
||||
if (value !== nextProps.value) {
|
||||
this.setState({ value: coerceAdhocMetrics(nextProps.value) });
|
||||
}
|
||||
}
|
||||
|
||||
onMetricEdit(changedMetric) {
|
||||
let newValue = this.state.value.map(value => {
|
||||
if (value.optionName === changedMetric.optionName) {
|
||||
return changedMetric;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
if (!this.props.multi) {
|
||||
newValue = newValue[0];
|
||||
}
|
||||
this.props.onChange(newValue);
|
||||
}
|
||||
|
||||
onChange(opts) {
|
||||
// if clear out options
|
||||
if (opts === null) {
|
||||
this.props.onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let transformedOpts = opts;
|
||||
if (!this.props.multi) {
|
||||
transformedOpts = [opts].filter(option => option);
|
||||
}
|
||||
let optionValues = transformedOpts
|
||||
.map(option => {
|
||||
if (option.metric_name) {
|
||||
return option.metric_name;
|
||||
} else if (option.column_name) {
|
||||
const clearedAggregate = this.clearedAggregateInInput;
|
||||
this.clearedAggregateInInput = null;
|
||||
return new AdhocMetric({
|
||||
column: option,
|
||||
aggregate: clearedAggregate || getDefaultAggregateForColumn(option),
|
||||
});
|
||||
} else if (option instanceof AdhocMetric) {
|
||||
return option;
|
||||
} else if (option.aggregate_name) {
|
||||
const newValue = `${option.aggregate_name}()`;
|
||||
this.select.setInputValue(newValue);
|
||||
this.select.handleInputChange({ target: { value: newValue } });
|
||||
// we need to set a timeout here or the selectionWill be overwritten
|
||||
// by some browsers (e.g. Chrome)
|
||||
setTimeout(() => {
|
||||
this.select.input.input.selectionStart = newValue.length - 1;
|
||||
this.select.input.input.selectionEnd = newValue.length - 1;
|
||||
}, 0);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(option => option);
|
||||
if (!this.props.multi) {
|
||||
optionValues = optionValues[0];
|
||||
}
|
||||
this.props.onChange(optionValues);
|
||||
}
|
||||
|
||||
checkIfAggregateInInput(input) {
|
||||
let nextState = { aggregateInInput: null };
|
||||
Object.keys(AGGREGATES).forEach(aggregate => {
|
||||
if (input.toLowerCase().startsWith(aggregate.toLowerCase() + '(')) {
|
||||
nextState = { aggregateInInput: aggregate };
|
||||
}
|
||||
});
|
||||
this.clearedAggregateInInput = this.state.aggregateInInput;
|
||||
this.setState(nextState);
|
||||
}
|
||||
|
||||
optionsForSelect(props) {
|
||||
const { columns, savedMetrics } = props;
|
||||
const aggregates =
|
||||
columns && columns.length
|
||||
? Object.keys(AGGREGATES).map(aggregate => ({
|
||||
aggregate_name: aggregate,
|
||||
}))
|
||||
: [];
|
||||
const options = [
|
||||
...(columns || []),
|
||||
...aggregates,
|
||||
...(savedMetrics || []),
|
||||
];
|
||||
|
||||
return options.reduce((results, option) => {
|
||||
if (option.metric_name) {
|
||||
results.push({ ...option, optionName: option.metric_name });
|
||||
} else if (option.column_name) {
|
||||
results.push({ ...option, optionName: '_col_' + option.column_name });
|
||||
} else if (option.aggregate_name) {
|
||||
results.push({
|
||||
...option,
|
||||
optionName: '_aggregate_' + option.aggregate_name,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, []);
|
||||
}
|
||||
|
||||
isAutoGeneratedMetric(savedMetric) {
|
||||
if (this.props.datasourceType === 'druid') {
|
||||
return druidAutoGeneratedMetricRegex.test(savedMetric.verbose_name);
|
||||
}
|
||||
return sqlaAutoGeneratedMetricNameRegex.test(savedMetric.metric_name);
|
||||
}
|
||||
|
||||
selectFilterOption(option, filterValue) {
|
||||
if (this.state.aggregateInInput) {
|
||||
let endIndex = filterValue.length;
|
||||
if (filterValue.endsWith(')')) {
|
||||
endIndex = filterValue.length - 1;
|
||||
}
|
||||
const valueAfterAggregate = filterValue.substring(
|
||||
filterValue.indexOf('(') + 1,
|
||||
endIndex,
|
||||
);
|
||||
return (
|
||||
option.column_name &&
|
||||
option.column_name.toLowerCase().indexOf(valueAfterAggregate) >= 0
|
||||
);
|
||||
}
|
||||
return (
|
||||
option.optionName &&
|
||||
(!option.metric_name ||
|
||||
!this.isAutoGeneratedMetric(option) ||
|
||||
option.verbose_name) &&
|
||||
(option.optionName.toLowerCase().indexOf(filterValue) >= 0 ||
|
||||
(option.verbose_name &&
|
||||
option.verbose_name.toLowerCase().indexOf(filterValue) >= 0))
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
// TODO figure out why the dropdown isnt appearing as soon as a metric is selected
|
||||
return (
|
||||
<div className="metrics-select">
|
||||
<ControlHeader {...this.props} />
|
||||
<OnPasteSelect
|
||||
multi={this.props.multi}
|
||||
name={`select-${this.props.name}`}
|
||||
placeholder={t('choose a column or aggregate function')}
|
||||
options={this.state.options}
|
||||
value={this.props.multi ? this.state.value : this.state.value[0]}
|
||||
labelKey="label"
|
||||
valueKey="optionName"
|
||||
clearable={this.props.clearable}
|
||||
closeOnSelect
|
||||
onChange={this.onChange}
|
||||
optionRenderer={this.optionRenderer}
|
||||
valueRenderer={this.valueRenderer}
|
||||
onInputChange={this.checkIfAggregateInInput}
|
||||
filterOption={this.selectFilterOption}
|
||||
refFunc={this.refFunc}
|
||||
selectWrap={VirtualizedSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MetricsControl.propTypes = propTypes;
|
||||
MetricsControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import Select from '../../../components/AsyncSelect';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import withToasts from '../../../messageToasts/enhancers/withToasts';
|
||||
|
||||
const propTypes = {
|
||||
dataEndpoint: PropTypes.string.isRequired,
|
||||
multi: PropTypes.bool,
|
||||
mutator: PropTypes.func,
|
||||
onAsyncErrorMessage: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.arrayOf(PropTypes.number),
|
||||
]),
|
||||
addDangerToast: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
multi: true,
|
||||
onAsyncErrorMessage: t('Error while fetching data'),
|
||||
onChange: () => {},
|
||||
placeholder: t('Select ...'),
|
||||
};
|
||||
|
||||
const SelectAsyncControl = props => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
dataEndpoint,
|
||||
multi,
|
||||
mutator,
|
||||
placeholder,
|
||||
onAsyncErrorMessage,
|
||||
} = props;
|
||||
const onSelectionChange = options => {
|
||||
let val;
|
||||
if (multi) {
|
||||
val = options.map(option => option.value);
|
||||
} else if (options) {
|
||||
val = options.value;
|
||||
} else {
|
||||
val = null;
|
||||
}
|
||||
onChange(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...props} />
|
||||
<Select
|
||||
dataEndpoint={dataEndpoint}
|
||||
onChange={onSelectionChange}
|
||||
onAsyncError={errorMsg =>
|
||||
this.props.addDangerToast(onAsyncErrorMessage + ': ' + errorMsg)
|
||||
}
|
||||
mutator={mutator}
|
||||
multi={multi}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
valueRenderer={v => <div>{v.label}</div>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SelectAsyncControl.propTypes = propTypes;
|
||||
SelectAsyncControl.defaultProps = defaultProps;
|
||||
|
||||
export default withToasts(SelectAsyncControl);
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import VirtualizedSelect from 'react-virtualized-select';
|
||||
import Select, { Creatable } from 'react-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import VirtualizedRendererWrap from '../../../components/VirtualizedRendererWrap';
|
||||
import OnPasteSelect from '../../../components/OnPasteSelect';
|
||||
|
||||
const propTypes = {
|
||||
choices: PropTypes.array,
|
||||
clearable: PropTypes.bool,
|
||||
description: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
freeForm: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
multi: PropTypes.bool,
|
||||
allowAll: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.array,
|
||||
]),
|
||||
showHeader: PropTypes.bool,
|
||||
optionRenderer: PropTypes.func,
|
||||
valueRenderer: PropTypes.func,
|
||||
valueKey: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
noResultsText: PropTypes.string,
|
||||
refFunc: PropTypes.func,
|
||||
filterOption: PropTypes.func,
|
||||
promptTextCreator: PropTypes.func,
|
||||
commaChoosesOption: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
choices: [],
|
||||
clearable: true,
|
||||
description: null,
|
||||
disabled: false,
|
||||
freeForm: false,
|
||||
isLoading: false,
|
||||
label: null,
|
||||
multi: false,
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
showHeader: true,
|
||||
optionRenderer: opt => opt.label,
|
||||
valueRenderer: opt => opt.label,
|
||||
valueKey: 'value',
|
||||
noResultsText: t('No results found'),
|
||||
promptTextCreator: label => `Create Option ${label}`,
|
||||
commaChoosesOption: true,
|
||||
allowAll: false,
|
||||
};
|
||||
|
||||
export default class SelectControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { options: this.getOptions(props) };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.createMetaSelectAllOption = this.createMetaSelectAllOption.bind(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
nextProps.choices !== this.props.choices ||
|
||||
nextProps.options !== this.props.options
|
||||
) {
|
||||
const options = this.getOptions(nextProps);
|
||||
this.setState({ options });
|
||||
}
|
||||
}
|
||||
|
||||
onChange(opt) {
|
||||
let optionValue = null;
|
||||
if (opt) {
|
||||
if (this.props.multi) {
|
||||
optionValue = [];
|
||||
for (const o of opt) {
|
||||
if (o.meta === true) {
|
||||
optionValue = this.getOptions(this.props)
|
||||
.filter(x => !x.meta)
|
||||
.map(x => x[this.props.valueKey]);
|
||||
break;
|
||||
} else {
|
||||
optionValue.push(o[this.props.valueKey]);
|
||||
}
|
||||
}
|
||||
} else if (opt.meta === true) {
|
||||
return;
|
||||
} else {
|
||||
optionValue = opt[this.props.valueKey];
|
||||
}
|
||||
}
|
||||
this.props.onChange(optionValue);
|
||||
}
|
||||
|
||||
getOptions(props) {
|
||||
let options = [];
|
||||
if (props.options) {
|
||||
options = props.options.map(x => x);
|
||||
} else {
|
||||
// Accepts different formats of input
|
||||
options = props.choices.map(c => {
|
||||
let option;
|
||||
if (Array.isArray(c)) {
|
||||
const label = c.length > 1 ? c[1] : c[0];
|
||||
option = { label };
|
||||
option[props.valueKey] = c[0];
|
||||
} else if (Object.is(c)) {
|
||||
option = c;
|
||||
} else {
|
||||
option = { label: c };
|
||||
option[props.valueKey] = c;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
}
|
||||
if (props.freeForm) {
|
||||
// For FreeFormSelect, insert value into options if not exist
|
||||
const values = options.map(c => c[props.valueKey]);
|
||||
if (props.value) {
|
||||
let valuesToAdd = props.value;
|
||||
if (!Array.isArray(valuesToAdd)) {
|
||||
valuesToAdd = [valuesToAdd];
|
||||
}
|
||||
valuesToAdd.forEach(v => {
|
||||
if (values.indexOf(v) < 0) {
|
||||
const toAdd = { label: v };
|
||||
toAdd[props.valueKey] = v;
|
||||
options.push(toAdd);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (props.allowAll === true && props.multi === true) {
|
||||
if (options.findIndex(o => this.isMetaSelectAllOption(o)) < 0) {
|
||||
options.unshift(this.createMetaSelectAllOption());
|
||||
}
|
||||
} else {
|
||||
options = options.filter(o => !this.isMetaSelectAllOption(o));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
isMetaSelectAllOption(o) {
|
||||
return o.meta && o.meta === true && o.label === 'Select All';
|
||||
}
|
||||
|
||||
createMetaSelectAllOption() {
|
||||
const option = { label: 'Select All', meta: true };
|
||||
option[this.props.valueKey] = 'Select All';
|
||||
return option;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
|
||||
const placeholder =
|
||||
this.props.placeholder || t('%s option(s)', this.state.options.length);
|
||||
const selectProps = {
|
||||
multi: this.props.multi,
|
||||
name: `select-${this.props.name}`,
|
||||
placeholder,
|
||||
options: this.state.options,
|
||||
value: this.props.value,
|
||||
labelKey: 'label',
|
||||
valueKey: this.props.valueKey,
|
||||
autosize: false,
|
||||
clearable: this.props.clearable,
|
||||
isLoading: this.props.isLoading,
|
||||
onChange: this.onChange,
|
||||
onFocus: this.props.onFocus,
|
||||
optionRenderer: VirtualizedRendererWrap(this.props.optionRenderer),
|
||||
valueRenderer: this.props.valueRenderer,
|
||||
noResultsText: this.props.noResultsText,
|
||||
disabled: this.props.disabled,
|
||||
refFunc: this.props.refFunc,
|
||||
filterOption: this.props.filterOption,
|
||||
promptTextCreator: this.props.promptTextCreator,
|
||||
ignoreAccents: false,
|
||||
};
|
||||
if (this.props.freeForm) {
|
||||
selectProps.selectComponent = Creatable;
|
||||
selectProps.shouldKeyDownEventCreateNewOption = key => {
|
||||
const keyCode = key.keyCode;
|
||||
if (this.props.commaChoosesOption && keyCode === 188) {
|
||||
return true;
|
||||
}
|
||||
return keyCode === 9 || keyCode === 13;
|
||||
};
|
||||
} else {
|
||||
selectProps.selectComponent = Select;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{this.props.showHeader && <ControlHeader {...this.props} />}
|
||||
<OnPasteSelect {...selectProps} selectWrap={VirtualizedSelect} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectControl.propTypes = propTypes;
|
||||
SelectControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import BootstrapSliderWrapper from '../../../components/BootstrapSliderWrapper';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
export default function SliderControl(props) {
|
||||
// This wouldn't be necessary but might as well
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...props} />
|
||||
<BootstrapSliderWrapper
|
||||
{...props}
|
||||
change={obj => {
|
||||
props.onChange(obj.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SliderControl.propTypes = propTypes;
|
||||
SliderControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import SelectControl from './SelectControl';
|
||||
import PopoverSection from '../../../components/PopoverSection';
|
||||
import Checkbox from '../../../components/Checkbox';
|
||||
|
||||
const spatialTypes = {
|
||||
latlong: 'latlong',
|
||||
delimited: 'delimited',
|
||||
geohash: 'geohash',
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object,
|
||||
animation: PropTypes.bool,
|
||||
choices: PropTypes.array,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
animation: true,
|
||||
choices: [],
|
||||
};
|
||||
|
||||
export default class SpatialControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const v = props.value || {};
|
||||
let defaultCol;
|
||||
if (props.choices.length > 0) {
|
||||
defaultCol = props.choices[0][0];
|
||||
}
|
||||
this.state = {
|
||||
type: v.type || spatialTypes.latlong,
|
||||
delimiter: v.delimiter || ',',
|
||||
latCol: v.latCol || defaultCol,
|
||||
lonCol: v.lonCol || defaultCol,
|
||||
lonlatCol: v.lonlatCol || defaultCol,
|
||||
reverseCheckbox: v.reverseCheckbox || false,
|
||||
geohashCol: v.geohashCol || defaultCol,
|
||||
value: null,
|
||||
errors: [],
|
||||
};
|
||||
this.toggleCheckbox = this.toggleCheckbox.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.renderReverseCheckbox = this.renderReverseCheckbox.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.onChange();
|
||||
}
|
||||
onChange() {
|
||||
const type = this.state.type;
|
||||
const value = { type };
|
||||
const errors = [];
|
||||
const errMsg = t('Invalid lat/long configuration.');
|
||||
if (type === spatialTypes.latlong) {
|
||||
value.latCol = this.state.latCol;
|
||||
value.lonCol = this.state.lonCol;
|
||||
if (!value.lonCol || !value.latCol) {
|
||||
errors.push(errMsg);
|
||||
}
|
||||
} else if (type === spatialTypes.delimited) {
|
||||
value.lonlatCol = this.state.lonlatCol;
|
||||
value.delimiter = this.state.delimiter;
|
||||
value.reverseCheckbox = this.state.reverseCheckbox;
|
||||
if (!value.lonlatCol || !value.delimiter) {
|
||||
errors.push(errMsg);
|
||||
}
|
||||
} else if (type === spatialTypes.geohash) {
|
||||
value.geohashCol = this.state.geohashCol;
|
||||
value.reverseCheckbox = this.state.reverseCheckbox;
|
||||
if (!value.geohashCol) {
|
||||
errors.push(errMsg);
|
||||
}
|
||||
}
|
||||
this.setState({ value, errors });
|
||||
this.props.onChange(value, errors);
|
||||
}
|
||||
setType(type) {
|
||||
this.setState({ type }, this.onChange);
|
||||
}
|
||||
close() {
|
||||
this.refs.trigger.hide();
|
||||
}
|
||||
toggleCheckbox() {
|
||||
this.setState(
|
||||
{ reverseCheckbox: !this.state.reverseCheckbox },
|
||||
this.onChange,
|
||||
);
|
||||
}
|
||||
renderLabelContent() {
|
||||
if (this.state.errors.length > 0) {
|
||||
return 'N/A';
|
||||
}
|
||||
if (this.state.type === spatialTypes.latlong) {
|
||||
return `${this.state.lonCol} | ${this.state.latCol}`;
|
||||
} else if (this.state.type === spatialTypes.delimited) {
|
||||
return `${this.state.lonlatCol}`;
|
||||
} else if (this.state.type === spatialTypes.geohash) {
|
||||
return `${this.state.geohashCol}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
renderSelect(name, type) {
|
||||
return (
|
||||
<SelectControl
|
||||
name={name}
|
||||
choices={this.props.choices}
|
||||
value={this.state[name]}
|
||||
clearable={false}
|
||||
onFocus={() => {
|
||||
this.setType(type);
|
||||
}}
|
||||
onChange={value => {
|
||||
this.setState({ [name]: value }, this.onChange);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
renderReverseCheckbox() {
|
||||
return (
|
||||
<span>
|
||||
{t('Reverse lat/long ')}
|
||||
<Checkbox
|
||||
checked={this.state.reverseCheckbox}
|
||||
onChange={this.toggleCheckbox}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
return (
|
||||
<Popover id="filter-popover">
|
||||
<div style={{ width: '300px' }}>
|
||||
<PopoverSection
|
||||
title={t('Longitude & Latitude columns')}
|
||||
isSelected={this.state.type === spatialTypes.latlong}
|
||||
onSelect={this.setType.bind(this, spatialTypes.latlong)}
|
||||
>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
Longitude
|
||||
{this.renderSelect('lonCol', spatialTypes.latlong)}
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
Latitude
|
||||
{this.renderSelect('latCol', spatialTypes.latlong)}
|
||||
</Col>
|
||||
</Row>
|
||||
</PopoverSection>
|
||||
<PopoverSection
|
||||
title={t('Delimited long & lat single column')}
|
||||
info={t(
|
||||
'Multiple formats accepted, look the geopy.points ' +
|
||||
'Python library for more details',
|
||||
)}
|
||||
isSelected={this.state.type === spatialTypes.delimited}
|
||||
onSelect={this.setType.bind(this, spatialTypes.delimited)}
|
||||
>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
{t('Column')}
|
||||
{this.renderSelect('lonlatCol', spatialTypes.delimited)}
|
||||
</Col>
|
||||
<Col md={6}>{this.renderReverseCheckbox()}</Col>
|
||||
</Row>
|
||||
</PopoverSection>
|
||||
<PopoverSection
|
||||
title={t('Geohash')}
|
||||
isSelected={this.state.type === spatialTypes.geohash}
|
||||
onSelect={this.setType.bind(this, spatialTypes.geohash)}
|
||||
>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
Column
|
||||
{this.renderSelect('geohashCol', spatialTypes.geohash)}
|
||||
</Col>
|
||||
<Col md={6}>{this.renderReverseCheckbox()}</Col>
|
||||
</Row>
|
||||
</PopoverSection>
|
||||
<div className="clearfix">
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="float-left ok"
|
||||
bsStyle="primary"
|
||||
onClick={this.close.bind(this)}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
animation={this.props.animation}
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<Label style={{ cursor: 'pointer' }}>
|
||||
{this.renderLabelContent()}
|
||||
</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SpatialControl.propTypes = propTypes;
|
||||
SpatialControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, FormGroup, FormControl } from 'react-bootstrap';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/html';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/mode/javascript';
|
||||
import 'brace/theme/textmate';
|
||||
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import ModalTrigger from '../../../components/ModalTrigger';
|
||||
|
||||
const propTypes = {
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
height: PropTypes.number,
|
||||
minLines: PropTypes.number,
|
||||
maxLines: PropTypes.number,
|
||||
offerEditInModal: PropTypes.bool,
|
||||
language: PropTypes.oneOf([
|
||||
null,
|
||||
'json',
|
||||
'html',
|
||||
'sql',
|
||||
'markdown',
|
||||
'javascript',
|
||||
]),
|
||||
aboveEditorSection: PropTypes.node,
|
||||
readOnly: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
value: '',
|
||||
height: 250,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
offerEditInModal: true,
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
export default class TextAreaControl extends React.Component {
|
||||
onControlChange(event) {
|
||||
this.props.onChange(event.target.value);
|
||||
}
|
||||
onAceChange(value) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
renderEditor(inModal = false) {
|
||||
const value = this.props.value || '';
|
||||
if (this.props.language) {
|
||||
return (
|
||||
<AceEditor
|
||||
mode={this.props.language}
|
||||
theme="textmate"
|
||||
style={{ border: '1px solid #CCC' }}
|
||||
minLines={inModal ? 40 : this.props.minLines}
|
||||
maxLines={inModal ? 1000 : this.props.maxLines}
|
||||
onChange={this.onAceChange.bind(this)}
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
value={value}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormGroup controlId="formControlsTextarea">
|
||||
<FormControl
|
||||
componentClass="textarea"
|
||||
placeholder={t('textarea')}
|
||||
onChange={this.onControlChange.bind(this)}
|
||||
value={value}
|
||||
disabled={this.props.readOnly}
|
||||
style={{ height: this.props.height }}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
renderModalBody() {
|
||||
return (
|
||||
<div>
|
||||
<div>{this.props.aboveEditorSection}</div>
|
||||
{this.renderEditor(true)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const controlHeader = <ControlHeader {...this.props} />;
|
||||
return (
|
||||
<div>
|
||||
{controlHeader}
|
||||
{this.renderEditor()}
|
||||
{this.props.offerEditInModal && (
|
||||
<ModalTrigger
|
||||
bsSize="large"
|
||||
modalTitle={controlHeader}
|
||||
triggerNode={
|
||||
<Button bsSize="small" className="m-t-5">
|
||||
{t('Edit')} <strong>{this.props.language}</strong>{' '}
|
||||
{t('in modal')}
|
||||
</Button>
|
||||
}
|
||||
modalBody={this.renderModalBody(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextAreaControl.propTypes = propTypes;
|
||||
TextAreaControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, FormControl } from 'react-bootstrap';
|
||||
import * as v from '../../validators';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
isFloat: PropTypes.bool,
|
||||
isInt: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
value: '',
|
||||
isInt: false,
|
||||
isFloat: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default class TextControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange(event) {
|
||||
let value = event.target.value;
|
||||
|
||||
// Validation & casting
|
||||
const errors = [];
|
||||
if (value !== '' && this.props.isFloat) {
|
||||
const error = v.numeric(value);
|
||||
if (error) {
|
||||
errors.push(error);
|
||||
} else {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
}
|
||||
if (value !== '' && this.props.isInt) {
|
||||
const error = v.integer(value);
|
||||
if (error) {
|
||||
errors.push(error);
|
||||
} else {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
}
|
||||
this.props.onChange(value, errors);
|
||||
}
|
||||
render() {
|
||||
const { value: rawValue } = this.props;
|
||||
const value =
|
||||
typeof rawValue !== 'undefined' && rawValue !== null
|
||||
? rawValue.toString()
|
||||
: '';
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<FormGroup controlId="formInlineName" bsSize="small">
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder=""
|
||||
onChange={this.onChange}
|
||||
onFocus={this.props.onFocus}
|
||||
value={value}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextControl.propTypes = propTypes;
|
||||
TextControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
FormControl,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import { t } from '@superset-ui/translation';
|
||||
|
||||
import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
|
||||
import BoundsControl from './BoundsControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
|
||||
const propTypes = {
|
||||
label: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
colType: PropTypes.string,
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
timeLag: PropTypes.string,
|
||||
timeRatio: PropTypes.string,
|
||||
comparisonType: PropTypes.string,
|
||||
showYAxis: PropTypes.bool,
|
||||
yAxisBounds: PropTypes.array,
|
||||
bounds: PropTypes.array,
|
||||
d3format: PropTypes.string,
|
||||
dateFormat: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
label: t('Time Series Columns'),
|
||||
tooltip: '',
|
||||
colType: '',
|
||||
width: '',
|
||||
height: '',
|
||||
timeLag: '',
|
||||
timeRatio: '',
|
||||
comparisonType: '',
|
||||
showYAxis: false,
|
||||
yAxisBounds: [null, null],
|
||||
bounds: [null, null],
|
||||
d3format: '',
|
||||
dateFormat: '',
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
const comparisonTypeOptions = [
|
||||
{ value: 'value', label: 'Actual value' },
|
||||
{ value: 'diff', label: 'Difference' },
|
||||
{ value: 'perc', label: 'Percentage' },
|
||||
{ value: 'perc_change', label: 'Percentage Change' },
|
||||
];
|
||||
|
||||
const colTypeOptions = [
|
||||
{ value: 'time', label: 'Time Comparison' },
|
||||
{ value: 'contrib', label: 'Contribution' },
|
||||
{ value: 'spark', label: 'Sparkline' },
|
||||
{ value: 'avg', label: 'Period Average' },
|
||||
];
|
||||
|
||||
export default class TimeSeriesColumnControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const state = {
|
||||
label: this.props.label,
|
||||
tooltip: this.props.tooltip,
|
||||
colType: this.props.colType,
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
timeLag: this.props.timeLag || 0,
|
||||
timeRatio: this.props.timeRatio,
|
||||
comparisonType: this.props.comparisonType,
|
||||
showYAxis: this.props.showYAxis,
|
||||
yAxisBounds: this.props.yAxisBounds,
|
||||
bounds: this.props.bounds,
|
||||
d3format: this.props.d3format,
|
||||
dateFormat: this.props.dateFormat,
|
||||
};
|
||||
delete state.onChange;
|
||||
this.state = state;
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange() {
|
||||
this.props.onChange(this.state);
|
||||
}
|
||||
onSelectChange(attr, opt) {
|
||||
this.setState({ [attr]: opt.value }, this.onChange);
|
||||
}
|
||||
onTextInputChange(attr, event) {
|
||||
this.setState({ [attr]: event.target.value }, this.onChange);
|
||||
}
|
||||
onCheckboxChange(attr, value) {
|
||||
this.setState({ [attr]: value }, this.onChange);
|
||||
}
|
||||
onBoundsChange(bounds) {
|
||||
this.setState({ bounds }, this.onChange);
|
||||
}
|
||||
onYAxisBoundsChange(yAxisBounds) {
|
||||
this.setState({ yAxisBounds }, this.onChange);
|
||||
}
|
||||
setType() {}
|
||||
textSummary() {
|
||||
return `${this.state.label}`;
|
||||
}
|
||||
edit() {}
|
||||
formRow(label, tooltip, ttLabel, control) {
|
||||
return (
|
||||
<Row style={{ marginTop: '5px' }}>
|
||||
<Col md={5}>
|
||||
{`${label} `}
|
||||
<InfoTooltipWithTrigger
|
||||
placement="top"
|
||||
tooltip={tooltip}
|
||||
label={ttLabel}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={7}>{control}</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
return (
|
||||
<Popover id="ts-col-popo" title="Column Configuration">
|
||||
<div style={{ width: 300 }}>
|
||||
{this.formRow(
|
||||
'Label',
|
||||
'The column header label',
|
||||
'time-lag',
|
||||
<FormControl
|
||||
value={this.state.label}
|
||||
onChange={this.onTextInputChange.bind(this, 'label')}
|
||||
bsSize="small"
|
||||
placeholder="Label"
|
||||
/>,
|
||||
)}
|
||||
{this.formRow(
|
||||
'Tooltip',
|
||||
'Column header tooltip',
|
||||
'col-tooltip',
|
||||
<FormControl
|
||||
value={this.state.tooltip}
|
||||
onChange={this.onTextInputChange.bind(this, 'tooltip')}
|
||||
bsSize="small"
|
||||
placeholder="Tooltip"
|
||||
/>,
|
||||
)}
|
||||
{this.formRow(
|
||||
'Type',
|
||||
'Type of comparison, value difference or percentage',
|
||||
'col-type',
|
||||
<Select
|
||||
value={this.state.colType}
|
||||
clearable={false}
|
||||
onChange={this.onSelectChange.bind(this, 'colType')}
|
||||
options={colTypeOptions}
|
||||
/>,
|
||||
)}
|
||||
<hr />
|
||||
{this.state.colType === 'spark' &&
|
||||
this.formRow(
|
||||
'Width',
|
||||
'Width of the sparkline',
|
||||
'spark-width',
|
||||
<FormControl
|
||||
value={this.state.width}
|
||||
onChange={this.onTextInputChange.bind(this, 'width')}
|
||||
bsSize="small"
|
||||
placeholder="Width"
|
||||
/>,
|
||||
)}
|
||||
{this.state.colType === 'spark' &&
|
||||
this.formRow(
|
||||
'Height',
|
||||
'Height of the sparkline',
|
||||
'spark-width',
|
||||
<FormControl
|
||||
value={this.state.height}
|
||||
onChange={this.onTextInputChange.bind(this, 'height')}
|
||||
bsSize="small"
|
||||
placeholder="height"
|
||||
/>,
|
||||
)}
|
||||
{['time', 'avg'].indexOf(this.state.colType) >= 0 &&
|
||||
this.formRow(
|
||||
'Time Lag',
|
||||
'Number of periods to compare against',
|
||||
'time-lag',
|
||||
<FormControl
|
||||
value={this.state.timeLag}
|
||||
onChange={this.onTextInputChange.bind(this, 'timeLag')}
|
||||
bsSize="small"
|
||||
placeholder="Time Lag"
|
||||
/>,
|
||||
)}
|
||||
{['spark'].indexOf(this.state.colType) >= 0 &&
|
||||
this.formRow(
|
||||
'Time Ratio',
|
||||
'Number of periods to ratio against',
|
||||
'time-ratio',
|
||||
<FormControl
|
||||
value={this.state.timeRatio}
|
||||
onChange={this.onTextInputChange.bind(this, 'timeRatio')}
|
||||
bsSize="small"
|
||||
placeholder="Time Ratio"
|
||||
/>,
|
||||
)}
|
||||
{this.state.colType === 'time' &&
|
||||
this.formRow(
|
||||
'Type',
|
||||
'Type of comparison, value difference or percentage',
|
||||
'comp-type',
|
||||
<Select
|
||||
value={this.state.comparisonType}
|
||||
clearable={false}
|
||||
onChange={this.onSelectChange.bind(this, 'comparisonType')}
|
||||
options={comparisonTypeOptions}
|
||||
/>,
|
||||
)}
|
||||
{this.state.colType === 'spark' &&
|
||||
this.formRow(
|
||||
'Show Y-axis',
|
||||
'Show Y-axis on the sparkline. Will display the manually set min/max if set or min/max values in the data otherwise.',
|
||||
'show-y-axis-bounds',
|
||||
<CheckboxControl
|
||||
value={this.state.showYAxis}
|
||||
onChange={this.onCheckboxChange.bind(this, 'showYAxis')}
|
||||
/>,
|
||||
)}
|
||||
{this.state.colType === 'spark' &&
|
||||
this.formRow(
|
||||
'Y-axis bounds',
|
||||
'Manually set min/max values for the y-axis.',
|
||||
'y-axis-bounds',
|
||||
<BoundsControl
|
||||
value={this.state.yAxisBounds}
|
||||
onChange={this.onYAxisBoundsChange.bind(this)}
|
||||
/>,
|
||||
)}
|
||||
{this.state.colType !== 'spark' &&
|
||||
this.formRow(
|
||||
'Color bounds',
|
||||
`Number bounds used for color encoding from red to blue.
|
||||
Reverse the numbers for blue to red. To get pure red or blue,
|
||||
you can enter either only min or max.`,
|
||||
'bounds',
|
||||
<BoundsControl
|
||||
value={this.state.bounds}
|
||||
onChange={this.onBoundsChange.bind(this)}
|
||||
/>,
|
||||
)}
|
||||
{this.formRow(
|
||||
'Number format',
|
||||
'Optional d3 number format string',
|
||||
'd3-format',
|
||||
<FormControl
|
||||
value={this.state.d3format}
|
||||
onChange={this.onTextInputChange.bind(this, 'd3format')}
|
||||
bsSize="small"
|
||||
placeholder="Number format string"
|
||||
/>,
|
||||
)}
|
||||
{this.state.colType === 'spark' &&
|
||||
this.formRow(
|
||||
'Date format',
|
||||
'Optional d3 date format string',
|
||||
'date-format',
|
||||
<FormControl
|
||||
value={this.state.dateFormat}
|
||||
onChange={this.onTextInputChange.bind(this, 'dateFormat')}
|
||||
bsSize="small"
|
||||
placeholder="Date format string"
|
||||
/>,
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
{this.textSummary()}{' '}
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<InfoTooltipWithTrigger
|
||||
icon="edit"
|
||||
className="text-primary"
|
||||
onClick={this.edit.bind(this)}
|
||||
label="edit-ts-column"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSeriesColumnControl.propTypes = propTypes;
|
||||
TimeSeriesColumnControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Label, Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
import { decimal2sexagesimal } from 'geolib';
|
||||
|
||||
import TextControl from './TextControl';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
export const DEFAULT_VIEWPORT = {
|
||||
longitude: 6.85236157047845,
|
||||
latitude: 31.222656842808707,
|
||||
zoom: 1,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
};
|
||||
|
||||
const PARAMS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch'];
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.shape({
|
||||
longitude: PropTypes.number,
|
||||
latitude: PropTypes.number,
|
||||
zoom: PropTypes.number,
|
||||
bearing: PropTypes.number,
|
||||
pitch: PropTypes.number,
|
||||
}),
|
||||
default: PropTypes.object,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
default: { type: 'fix', value: 5 },
|
||||
value: DEFAULT_VIEWPORT,
|
||||
};
|
||||
|
||||
export default class ViewportControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange(ctrl, value) {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
[ctrl]: value,
|
||||
});
|
||||
}
|
||||
renderTextControl(ctrl) {
|
||||
return (
|
||||
<div key={ctrl}>
|
||||
{ctrl}
|
||||
<TextControl
|
||||
value={this.props.value[ctrl]}
|
||||
onChange={this.onChange.bind(this, ctrl)}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
renderPopover() {
|
||||
return (
|
||||
<Popover id={`filter-popover-${this.props.name}`} title="Viewport">
|
||||
{PARAMS.map(ctrl => this.renderTextControl(ctrl))}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
renderLabel() {
|
||||
if (this.props.value.longitude && this.props.value.latitude) {
|
||||
return (
|
||||
decimal2sexagesimal(this.props.value.longitude) +
|
||||
' | ' +
|
||||
decimal2sexagesimal(this.props.value.latitude)
|
||||
);
|
||||
}
|
||||
return 'N/A';
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
container={document.body}
|
||||
trigger="click"
|
||||
rootClose
|
||||
ref="trigger"
|
||||
placement="right"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<Label style={{ cursor: 'pointer' }}>{this.renderLabel()}</Label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewportControl.propTypes = propTypes;
|
||||
ViewportControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Label,
|
||||
Row,
|
||||
Col,
|
||||
FormControl,
|
||||
Modal,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { getChartMetadataRegistry } from '@superset-ui/chart';
|
||||
|
||||
import ControlHeader from '../ControlHeader';
|
||||
import './VizTypeControl.less';
|
||||
|
||||
const propTypes = {
|
||||
description: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
const registry = getChartMetadataRegistry();
|
||||
|
||||
const IMAGE_PER_ROW = 6;
|
||||
const LABEL_STYLE = { cursor: 'pointer' };
|
||||
const DEFAULT_ORDER = [
|
||||
'line',
|
||||
'big_number',
|
||||
'table',
|
||||
'filter_box',
|
||||
'dist_bar',
|
||||
'area',
|
||||
'bar',
|
||||
'deck_polygon',
|
||||
'pie',
|
||||
'time_table',
|
||||
'pivot_table',
|
||||
'histogram',
|
||||
'big_number_total',
|
||||
'deck_scatter',
|
||||
'deck_hex',
|
||||
'time_pivot',
|
||||
'deck_arc',
|
||||
'heatmap',
|
||||
'deck_grid',
|
||||
'dual_line',
|
||||
'deck_screengrid',
|
||||
'line_multi',
|
||||
'treemap',
|
||||
'box_plot',
|
||||
'separator',
|
||||
'sunburst',
|
||||
'sankey',
|
||||
'word_cloud',
|
||||
'mapbox',
|
||||
'kepler',
|
||||
'cal_heatmap',
|
||||
'rose',
|
||||
'bubble',
|
||||
'deck_geojson',
|
||||
'horizon',
|
||||
'markup',
|
||||
'deck_multi',
|
||||
'compare',
|
||||
'partition',
|
||||
'event_flow',
|
||||
'deck_path',
|
||||
'directed_force',
|
||||
'world_map',
|
||||
'paired_ttest',
|
||||
'para',
|
||||
'iframe',
|
||||
'country_map',
|
||||
];
|
||||
|
||||
const typesWithDefaultOrder = new Set(DEFAULT_ORDER);
|
||||
|
||||
export default class VizTypeControl extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showModal: false,
|
||||
filter: '',
|
||||
};
|
||||
this.toggleModal = this.toggleModal.bind(this);
|
||||
this.changeSearch = this.changeSearch.bind(this);
|
||||
this.setSearchRef = this.setSearchRef.bind(this);
|
||||
this.focusSearch = this.focusSearch.bind(this);
|
||||
}
|
||||
|
||||
onChange(vizType) {
|
||||
this.props.onChange(vizType);
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
|
||||
setSearchRef(searchRef) {
|
||||
this.searchRef = searchRef;
|
||||
}
|
||||
|
||||
toggleModal() {
|
||||
this.setState({ showModal: !this.state.showModal });
|
||||
}
|
||||
|
||||
changeSearch(event) {
|
||||
this.setState({ filter: event.target.value });
|
||||
}
|
||||
|
||||
focusSearch() {
|
||||
if (this.searchRef) {
|
||||
this.searchRef.focus();
|
||||
}
|
||||
}
|
||||
|
||||
renderItem(entry) {
|
||||
const { value } = this.props;
|
||||
const { key, value: type } = entry;
|
||||
const isSelected = key === value;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`viztype-selector-container ${isSelected ? 'selected' : ''}`}
|
||||
onClick={this.onChange.bind(this, key)}
|
||||
>
|
||||
<img
|
||||
alt={type.name}
|
||||
width="100%"
|
||||
className={`viztype-selector ${isSelected ? 'selected' : ''}`}
|
||||
src={type.thumbnail}
|
||||
/>
|
||||
<div className="viztype-label">{type.name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, showModal } = this.state;
|
||||
const { value } = this.props;
|
||||
|
||||
const filterString = filter.toLowerCase();
|
||||
const filteredTypes = DEFAULT_ORDER.filter(type => registry.has(type))
|
||||
.map(type => ({
|
||||
key: type,
|
||||
value: registry.get(type),
|
||||
}))
|
||||
.concat(
|
||||
registry.entries().filter(({ key }) => !typesWithDefaultOrder.has(key)),
|
||||
)
|
||||
.filter(entry => entry.value.name.toLowerCase().includes(filterString));
|
||||
|
||||
const rows = [];
|
||||
for (let i = 0; i <= filteredTypes.length; i += IMAGE_PER_ROW) {
|
||||
rows.push(
|
||||
<Row key={`row-${i}`}>
|
||||
{filteredTypes.slice(i, i + IMAGE_PER_ROW).map(entry => (
|
||||
<Col md={12 / IMAGE_PER_ROW} key={`grid-col-${entry.key}`}>
|
||||
{this.renderItem(entry)}
|
||||
</Col>
|
||||
))}
|
||||
</Row>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader {...this.props} />
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id="error-tooltip">
|
||||
{t('Click to change visualization type')}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Label onClick={this.toggleModal} style={LABEL_STYLE}>
|
||||
{registry.has(value) ? registry.get(value).name : `${value}`}
|
||||
</Label>
|
||||
{!registry.has(value) && (
|
||||
<div className="text-danger">
|
||||
<i className="fa fa-exclamation-circle text-danger" />{' '}
|
||||
<small>{t('This visualization type is not supported.')}</small>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</OverlayTrigger>
|
||||
<Modal
|
||||
show={showModal}
|
||||
onHide={this.toggleModal}
|
||||
onEnter={this.focusSearch}
|
||||
onExit={this.setSearchRef}
|
||||
bsSize="lg"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('Select a visualization type')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="viztype-control-search-box">
|
||||
<FormControl
|
||||
inputRef={this.setSearchRef}
|
||||
type="text"
|
||||
value={filter}
|
||||
placeholder={t('Search')}
|
||||
onChange={this.changeSearch}
|
||||
/>
|
||||
</div>
|
||||
{rows}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VizTypeControl.propTypes = propTypes;
|
||||
VizTypeControl.defaultProps = defaultProps;
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 '../../../../stylesheets/less/variables.less';
|
||||
|
||||
.viztype-label {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: @font-size-m;
|
||||
}
|
||||
|
||||
.viztype-selector-container {
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover img {
|
||||
border: 1px solid @gray-heading;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
|
||||
img {
|
||||
border: 1px solid @almost-black;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
border: 1px solid @gray-light;
|
||||
border-radius: @border-radius-large;
|
||||
transition: border-color @timing-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.viztype-control-search-box {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
84
superset-frontend/src/explore/components/controls/index.js
Normal file
84
superset-frontend/src/explore/components/controls/index.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 AnnotationLayerControl from './AnnotationLayerControl';
|
||||
import BoundsControl from './BoundsControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
import CollectionControl from './CollectionControl';
|
||||
import ColorMapControl from './ColorMapControl';
|
||||
import ColorPickerControl from './ColorPickerControl';
|
||||
import ColorSchemeControl from './ColorSchemeControl';
|
||||
import DatasourceControl from './DatasourceControl';
|
||||
import DateFilterControl from './DateFilterControl';
|
||||
import FixedOrMetricControl from './FixedOrMetricControl';
|
||||
import HiddenControl from './HiddenControl';
|
||||
import SelectAsyncControl from './SelectAsyncControl';
|
||||
import SelectControl from './SelectControl';
|
||||
import SliderControl from './SliderControl';
|
||||
import SpatialControl from './SpatialControl';
|
||||
import TextAreaControl from './TextAreaControl';
|
||||
import TextControl from './TextControl';
|
||||
import TimeSeriesColumnControl from './TimeSeriesColumnControl';
|
||||
import ViewportControl from './ViewportControl';
|
||||
import VizTypeControl from './VizTypeControl';
|
||||
import MetricsControl from './MetricsControl';
|
||||
import AdhocFilterControl from './AdhocFilterControl';
|
||||
import FilterBoxItemControl from './FilterBoxItemControl';
|
||||
import withVerification from './withVerification';
|
||||
|
||||
const controlMap = {
|
||||
AnnotationLayerControl,
|
||||
BoundsControl,
|
||||
CheckboxControl,
|
||||
CollectionControl,
|
||||
ColorMapControl,
|
||||
ColorPickerControl,
|
||||
ColorSchemeControl,
|
||||
DatasourceControl,
|
||||
DateFilterControl,
|
||||
FixedOrMetricControl,
|
||||
HiddenControl,
|
||||
SelectAsyncControl,
|
||||
SelectControl,
|
||||
SliderControl,
|
||||
SpatialControl,
|
||||
TextAreaControl,
|
||||
TextControl,
|
||||
TimeSeriesColumnControl,
|
||||
ViewportControl,
|
||||
VizTypeControl,
|
||||
MetricsControl,
|
||||
AdhocFilterControl,
|
||||
FilterBoxItemControl,
|
||||
MetricsControlVerifiedOptions: withVerification(
|
||||
MetricsControl,
|
||||
'metric_name',
|
||||
'savedMetrics',
|
||||
),
|
||||
SelectControlVerifiedOptions: withVerification(
|
||||
SelectControl,
|
||||
'column_name',
|
||||
'options',
|
||||
),
|
||||
AdhocFilterControlVerifiedOptions: withVerification(
|
||||
AdhocFilterControl,
|
||||
'column_name',
|
||||
'columns',
|
||||
),
|
||||
};
|
||||
export default controlMap;
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export default function withVerification(
|
||||
WrappedComponent,
|
||||
optionLabel,
|
||||
optionsName,
|
||||
) {
|
||||
/*
|
||||
* This function will verify control options before passing them to the control by calling an
|
||||
* endpoint on mount and when the controlValues change. controlValues should be set in
|
||||
* mapStateToProps that can be added as a control override along with getEndpoint.
|
||||
*/
|
||||
class withVerificationComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
validOptions: null,
|
||||
hasRunVerification: false,
|
||||
};
|
||||
|
||||
this.getValidOptions = this.getValidOptions.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getValidOptions();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { hasRunVerification } = this.state;
|
||||
if (
|
||||
!isEqual(this.props.controlValues, prevProps.controlValues) ||
|
||||
!hasRunVerification
|
||||
) {
|
||||
this.getValidOptions();
|
||||
}
|
||||
}
|
||||
|
||||
getValidOptions() {
|
||||
const endpoint = this.props.getEndpoint(this.props.controlValues);
|
||||
if (endpoint) {
|
||||
SupersetClient.get({
|
||||
endpoint,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (Array.isArray(json)) {
|
||||
this.setState({ validOptions: new Set(json) || new Set() });
|
||||
}
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
|
||||
if (!this.state.hasRunVerification) {
|
||||
this.setState({ hasRunVerification: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { validOptions } = this.state;
|
||||
const options = this.props[optionsName];
|
||||
const verifiedOptions = validOptions
|
||||
? options.filter(o => validOptions.has(o[optionLabel]))
|
||||
: options;
|
||||
|
||||
const newProps = { ...this.props, [optionsName]: verifiedOptions };
|
||||
|
||||
return <WrappedComponent {...newProps} />;
|
||||
}
|
||||
}
|
||||
withVerificationComponent.propTypes = WrappedComponent.propTypes;
|
||||
return withVerificationComponent;
|
||||
}
|
||||
Reference in New Issue
Block a user