mirror of
https://github.com/apache/superset.git
synced 2026-05-11 02:45:46 +00:00
refactor(explore): convert ControlPanelsContainer to typescript (#13221)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user