refactor(explore): convert ControlPanelsContainer to typescript (#13221)

This commit is contained in:
Jesse Yang
2021-02-28 08:10:15 -10:00
committed by GitHub
parent 892eef1af6
commit 3c62069bbb
27 changed files with 385 additions and 326 deletions

View File

@@ -69,12 +69,6 @@ export default class CollectionControl extends React.Component {
this.onAdd = this.onAdd.bind(this);
}
componentDidUpdate(prevProps) {
if (prevProps.datasource.name !== this.props.datasource.name) {
this.props.onChange([]);
}
}
onChange(i, value) {
Object.assign(this.props.value[i], value);
this.props.onChange(this.props.value);

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import rison from 'rison';
import {
SupersetClient,
@@ -25,6 +25,7 @@ import {
t,
TimeRangeEndpoints,
} from '@superset-ui/core';
import { DatasourceMeta } from '@superset-ui/chart-controls';
import {
buildTimeRangeString,
formatTimeRange,
@@ -38,6 +39,8 @@ import { Divider } from 'src/common/components';
import Icon from 'src/components/Icon';
import { Select } from 'src/components/Select';
import { Tooltip } from 'src/common/components/Tooltip';
import { DEFAULT_TIME_RANGE } from 'src/explore/constants';
import { SelectOptionType, FrameType } from './types';
import {
COMMON_RANGE_VALUES_SET,
@@ -165,28 +168,27 @@ const IconWrapper = styled.span`
}
`;
interface DateFilterLabelProps {
interface DateFilterControlProps {
name: string;
onChange: (timeRange: string) => void;
value?: string;
endpoints?: TimeRangeEndpoints;
datasource?: string;
datasource?: DatasourceMeta;
}
export default function DateFilterControl(props: DateFilterLabelProps) {
const { value = 'Last week', endpoints, onChange, datasource } = props;
export default function DateFilterControl(props: DateFilterControlProps) {
const { value = DEFAULT_TIME_RANGE, endpoints, onChange } = props;
const [actualTimeRange, setActualTimeRange] = useState<string>(value);
const [show, setShow] = useState<boolean>(false);
const [frame, setFrame] = useState<FrameType>(guessFrame(value));
const [isMounted, setIsMounted] = useState<boolean>(false);
const guessedFrame = useMemo(() => guessFrame(value), [value]);
const [frame, setFrame] = useState<FrameType>(guessedFrame);
const [timeRangeValue, setTimeRangeValue] = useState(value);
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
const [evalResponse, setEvalResponse] = useState<string>(value);
const [tooltipTitle, setTooltipTitle] = useState<string>(value);
useEffect(() => {
if (!isMounted) setIsMounted(true);
fetchTimeRange(value, endpoints).then(({ value: actualRange, error }) => {
if (error) {
setEvalResponse(error || '');
@@ -205,9 +207,9 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
+--------------+------+----------+--------+----------+-----------+
*/
if (
frame === 'Common' ||
frame === 'Calendar' ||
frame === 'No filter'
guessedFrame === 'Common' ||
guessedFrame === 'Calendar' ||
guessedFrame === 'No filter'
) {
setActualTimeRange(value);
setTooltipTitle(actualRange || '');
@@ -220,14 +222,6 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
});
}, [value]);
useEffect(() => {
if (isMounted) {
onChange('Last week');
setTimeRangeValue('Last week');
setFrame(guessFrame('Last week'));
}
}, [datasource]);
useEffect(() => {
fetchTimeRange(timeRangeValue, endpoints).then(({ value, error }) => {
if (error) {
@@ -247,13 +241,13 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
function onOpen() {
setTimeRangeValue(value);
setFrame(guessFrame(value));
setFrame(guessedFrame);
setShow(true);
}
function onHide() {
setTimeRangeValue(value);
setFrame(guessFrame(value));
setFrame(guessedFrame);
setShow(false);
}
@@ -265,7 +259,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
}
};
function onFrame(option: SelectOptionType) {
function onChangeFrame(option: SelectOptionType) {
if (option.value === 'No filter') {
setTimeRangeValue('No filter');
}
@@ -278,7 +272,7 @@ export default function DateFilterControl(props: DateFilterLabelProps) {
<Select
options={FRAME_OPTIONS}
value={FRAME_OPTIONS.filter(({ value }) => value === frame)}
onChange={onFrame}
onChange={onChangeFrame}
className="frame-dropdown"
/>
{frame !== 'No filter' && <Divider />}

View File

@@ -18,7 +18,13 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { t, logging, SupersetClient, withTheme } from '@superset-ui/core';
import {
t,
logging,
SupersetClient,
withTheme,
ensureIsArray,
} from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
@@ -39,6 +45,11 @@ import AdhocFilterOption from './AdhocFilterOption';
import AdhocFilter, { CLAUSES, EXPRESSION_TYPES } from './AdhocFilter';
import adhocFilterType from './adhocFilterType';
const selectedMetricType = PropTypes.oneOfType([
PropTypes.string,
adhocMetricType,
]);
const propTypes = {
name: PropTypes.string,
onChange: PropTypes.func,
@@ -46,12 +57,10 @@ const propTypes = {
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]),
),
}),
selectedMetrics: PropTypes.oneOfType([
selectedMetricType,
PropTypes.arrayOf(selectedMetricType),
]),
isLoading: PropTypes.bool,
};
@@ -60,7 +69,7 @@ const defaultProps = {
onChange: () => {},
columns: [],
savedMetrics: [],
formData: {},
selectedMetrics: [],
};
function isDictionaryForAdhocFilter(value) {
@@ -141,10 +150,7 @@ class AdhocFilterControl extends React.Component {
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (
this.props.columns !== nextProps.columns ||
this.props.formData !== nextProps.formData
) {
if (this.props.columns !== nextProps.columns) {
this.setState({ options: this.optionsForSelect(nextProps) });
}
if (this.props.value !== nextProps.value) {
@@ -270,7 +276,7 @@ class AdhocFilterControl extends React.Component {
optionsForSelect(props) {
const options = [
...props.columns,
...[...(props.formData?.metrics || []), props.formData?.metric].map(
...ensureIsArray(props.selectedMetrics).map(
metric =>
metric &&
(typeof metric === 'string'

View File

@@ -123,9 +123,6 @@ export default class AdhocMetricEditPopover extends React.PureComponent {
adhocMetricLabel: this.state.adhocMetric?.getDefaultLabel(),
});
}
if (prevProps.datasource !== this.props.datasource) {
this.props.onChange(null);
}
}
componentWillUnmount() {

View File

@@ -60,7 +60,6 @@ class AdhocMetricOption extends React.PureComponent {
onMoveLabel,
onDropLabel,
index,
datasource,
} = this.props;
return (
@@ -70,7 +69,6 @@ class AdhocMetricOption extends React.PureComponent {
columns={columns}
savedMetricsOptions={savedMetricsOptions}
savedMetric={savedMetric}
datasource={datasource}
datasourceType={datasourceType}
>
<OptionControlLabel

View File

@@ -33,7 +33,6 @@ export type AdhocMetricPopoverTriggerProps = {
savedMetricsOptions: savedMetricType[];
savedMetric: savedMetricType;
datasourceType: string;
datasource: string;
children: ReactNode;
createNew?: boolean;
};
@@ -160,7 +159,6 @@ class AdhocMetricPopoverTrigger extends React.PureComponent<
columns={this.props.columns}
savedMetricsOptions={this.props.savedMetricsOptions}
savedMetric={this.props.savedMetric}
datasource={this.props.datasource}
datasourceType={this.props.datasourceType}
onResize={this.onPopoverResize}
onClose={this.closePopover}

View File

@@ -38,7 +38,6 @@ const propTypes = {
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
multi: PropTypes.bool,
datasourceType: PropTypes.string,
datasource: PropTypes.string,
};
export default function MetricDefinitionValue({
@@ -52,16 +51,15 @@ export default function MetricDefinitionValue({
onMoveLabel,
onDropLabel,
index,
datasource,
}) {
const getSavedMetricByName = metricName =>
savedMetrics.find(metric => metric.metric_name === metricName);
let savedMetric;
if (option.metric_name) {
savedMetric = option;
} else if (typeof option === 'string') {
if (typeof option === 'string') {
savedMetric = getSavedMetricByName(option);
} else if (option.metric_name) {
savedMetric = option;
}
if (option instanceof AdhocMetric || savedMetric) {
@@ -79,7 +77,6 @@ export default function MetricDefinitionValue({
onDropLabel,
index,
savedMetric: savedMetric ?? {},
datasource,
};
return <AdhocMetricOption {...metricOptionProps} />;

View File

@@ -179,10 +179,10 @@ class MetricsControl extends React.PureComponent {
) {
this.setState({ options: this.optionsForSelect(nextProps) });
// Remove metrics if selected value no longer a column
const containsAllMetrics = columnsContainAllMetrics(value, nextProps);
if (!containsAllMetrics) {
// Remove all metrics if selected value no longer a valid column
// in the dataset. Must use `nextProps` here because Redux reducers may
// have already updated the value for this control.
if (!columnsContainAllMetrics(nextProps.value, nextProps)) {
this.props.onChange([]);
}
}

View File

@@ -17,71 +17,55 @@
* under the License.
*/
import React from 'react';
import { FormGroup, FormControl } from 'react-bootstrap';
import { FormGroup, FormControl, FormControlProps } from 'react-bootstrap';
import { legacyValidateNumber, legacyValidateInteger } from '@superset-ui/core';
import debounce from 'lodash/debounce';
import ControlHeader from '../ControlHeader';
import { FAST_DEBOUNCE } from 'src/constants';
import ControlHeader from 'src/explore/components/ControlHeader';
interface TextControlProps {
type InputValueType = string | number;
export interface TextControlProps<T extends InputValueType = InputValueType> {
disabled?: boolean;
isFloat?: boolean;
isInt?: boolean;
onChange?: (value: any, errors: any) => {};
onChange?: (value: T, errors: any) => {};
onFocus?: () => {};
placeholder?: string;
value?: string | number;
value?: T | null;
controlId?: string;
renderTrigger?: boolean;
datasource?: string;
}
interface TextControlState {
export interface TextControlState {
controlId: string;
currentDatasource?: string;
value?: string | number;
value: string;
}
const generateControlId = (controlId?: string) =>
`formInlineName_${controlId ?? (Math.random() * 1000000).toFixed()}`;
export default class TextControl extends React.Component<
TextControlProps,
TextControlState
> {
debouncedOnChange = debounce((inputValue: string) => {
this.onChange(inputValue);
}, 500);
const safeStringify = (value?: InputValueType | null) =>
value == null ? '' : String(value);
static getDerivedStateFromProps(
props: TextControlProps,
state: TextControlState,
) {
// reset value when datasource changes
// props.datasource and props.value don't update in the same re-render,
// so we need to synchronize them to update the state with correct values
if (
props.value !== state.value &&
props.datasource !== state.currentDatasource
) {
return { value: props.value, currentDatasource: props.datasource };
}
return null;
}
export default class TextControl<
T extends InputValueType = InputValueType
> extends React.Component<TextControlProps<T>, TextControlState> {
initialValue?: TextControlProps['value'];
constructor(props: TextControlProps) {
constructor(props: TextControlProps<T>) {
super(props);
// if there's no control id provided, generate a random
// number to prevent rendering elements with same ids
this.initialValue = props.value;
this.state = {
// if there's no control id provided, generate a random
// number to prevent rendering elements with same ids
controlId: generateControlId(props.controlId),
value: props.value,
currentDatasource: props.datasource,
value: safeStringify(this.initialValue),
};
}
onChange = (inputValue: string) => {
let parsedValue: string | number = inputValue;
let parsedValue: InputValueType = inputValue;
// Validation & casting
const errors = [];
if (inputValue !== '' && this.props.isFloat) {
@@ -102,26 +86,26 @@ export default class TextControl extends React.Component<
parsedValue = parseInt(inputValue, 10);
}
}
this.props.onChange?.(parsedValue, errors);
this.props.onChange?.(parsedValue as T, errors);
};
onChangeWrapper = (event: any) => {
const { value } = event.target;
this.setState({ value });
debouncedOnChange = debounce((inputValue: string) => {
this.onChange(inputValue);
}, FAST_DEBOUNCE);
// use debounce when change takes effect immediately after user starts typing
const onChange = this.props.renderTrigger
? this.debouncedOnChange
: this.onChange;
onChange(value);
onChangeWrapper: FormControlProps['onChange'] = event => {
const { value } = event.target as HTMLInputElement;
this.setState({ value }, () => {
this.debouncedOnChange(value);
});
};
render = () => {
const { value: rawValue } = this.state;
const value =
typeof rawValue !== 'undefined' && rawValue !== null
? rawValue.toString()
: '';
let { value } = this.state;
if (this.initialValue !== this.props.value) {
this.initialValue = this.props.value;
value = safeStringify(this.props.value);
}
return (
<div>
<ControlHeader {...this.props} />