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