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:
David Aaron Suddjian
2020-02-09 17:53:56 -08:00
committed by GitHub
parent 0cf354cc88
commit 2913063924
930 changed files with 681 additions and 314 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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" /> &nbsp; {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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}`,
}}
>
&nbsp;
</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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View 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;

View File

@@ -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;
}