mirror of
https://github.com/apache/superset.git
synced 2026-05-11 10:55:43 +00:00
refactor(explore): convert ControlPanelsContainer to typescript (#13221)
This commit is contained in:
@@ -18,30 +18,47 @@
|
||||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { t, styled, getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
import {
|
||||
t,
|
||||
styled,
|
||||
getChartControlPanelRegistry,
|
||||
QueryFormData,
|
||||
DatasourceType,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import Tabs from 'src/common/components/Tabs';
|
||||
import Alert from 'src/components/Alert';
|
||||
import Collapse from 'src/common/components/Collapse';
|
||||
import { PluginContext } from 'src/components/DynamicPlugins';
|
||||
import Loading from 'src/components/Loading';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
ControlPanelSectionConfig,
|
||||
ControlState,
|
||||
CustomControlItem,
|
||||
ExpandedControlItem,
|
||||
InfoTooltipWithTrigger,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import ControlRow from './ControlRow';
|
||||
import Control from './Control';
|
||||
import { sectionsToRender } from '../controlUtils';
|
||||
import { exploreActions } from '../actions/exploreActions';
|
||||
import { ExploreActions, exploreActions } from '../actions/exploreActions';
|
||||
import { ExploreState } from '../reducers/getInitialState';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
alert: PropTypes.string,
|
||||
datasource_type: PropTypes.string.isRequired,
|
||||
exploreState: PropTypes.object.isRequired,
|
||||
controls: PropTypes.object.isRequired,
|
||||
form_data: PropTypes.object.isRequired,
|
||||
isDatasourceMetaLoading: PropTypes.bool.isRequired,
|
||||
export type ControlPanelsContainerProps = {
|
||||
actions: ExploreActions;
|
||||
datasource_type: DatasourceType;
|
||||
exploreState: Record<string, any>;
|
||||
controls: Record<string, ControlState>;
|
||||
form_data: QueryFormData;
|
||||
isDatasourceMetaLoading: boolean;
|
||||
};
|
||||
|
||||
export type ExpandedControlPanelSectionConfig = Omit<
|
||||
ControlPanelSectionConfig,
|
||||
'controlSetRows'
|
||||
> & {
|
||||
controlSetRows: ExpandedControlItem[][];
|
||||
};
|
||||
|
||||
const Styles = styled.div`
|
||||
@@ -50,9 +67,6 @@ const Styles = styled.div`
|
||||
overflow: auto;
|
||||
overflow-x: visible;
|
||||
overflow-y: auto;
|
||||
.remove-alert {
|
||||
cursor: pointer;
|
||||
}
|
||||
#controlSections {
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
@@ -80,60 +94,34 @@ const ControlPanelsTabs = styled(Tabs)`
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
class ControlPanelsContainer extends React.Component {
|
||||
|
||||
class ControlPanelsContainer extends React.Component<ControlPanelsContainerProps> {
|
||||
// trigger updates to the component when async plugins load
|
||||
static contextType = PluginContext;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: ControlPanelsContainerProps) {
|
||||
super(props);
|
||||
|
||||
this.removeAlert = this.removeAlert.bind(this);
|
||||
this.renderControl = this.renderControl.bind(this);
|
||||
this.renderControlPanelSection = this.renderControlPanelSection.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
actions: { setControlValue },
|
||||
} = this.props;
|
||||
if (this.props.form_data.datasource !== prevProps.form_data.datasource) {
|
||||
const defaultValues = [
|
||||
'MetricsControl',
|
||||
'AdhocFilterControl',
|
||||
'TextControl',
|
||||
'SelectControl',
|
||||
'CheckboxControl',
|
||||
'AnnotationLayerControl',
|
||||
];
|
||||
Object.entries(this.props.controls).forEach(([controlName, control]) => {
|
||||
const { type, default: defaultValue } = control;
|
||||
if (defaultValues.includes(type)) {
|
||||
setControlValue(controlName, defaultValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sectionsToRender() {
|
||||
sectionsToRender(): ExpandedControlPanelSectionConfig[] {
|
||||
return sectionsToRender(
|
||||
this.props.form_data.viz_type,
|
||||
this.props.datasource_type,
|
||||
);
|
||||
}
|
||||
|
||||
sectionsToExpand(sections) {
|
||||
sectionsToExpand(sections: ControlPanelSectionConfig[]) {
|
||||
return sections.reduce(
|
||||
(acc, cur) => (cur.expanded ? [...acc, cur.label] : acc),
|
||||
[],
|
||||
(acc, section) =>
|
||||
section.expanded ? [...acc, String(section.label)] : acc,
|
||||
[] as string[],
|
||||
);
|
||||
}
|
||||
|
||||
removeAlert() {
|
||||
this.props.actions.removeControlPanelAlert();
|
||||
}
|
||||
|
||||
renderControl({ name, config }) {
|
||||
const { actions, controls, form_data: formData } = this.props;
|
||||
renderControl({ name, config }: CustomControlItem) {
|
||||
const { actions, controls } = this.props;
|
||||
const { visibility } = config;
|
||||
|
||||
// If the control item is not an object, we have to look up the control data from
|
||||
@@ -144,11 +132,9 @@ class ControlPanelsContainer extends React.Component {
|
||||
...controls[name],
|
||||
name,
|
||||
};
|
||||
const {
|
||||
validationErrors,
|
||||
provideFormDataToProps,
|
||||
...restProps
|
||||
} = controlData;
|
||||
const { validationErrors, ...restProps } = controlData as ControlState & {
|
||||
validationErrors?: any[];
|
||||
};
|
||||
|
||||
// if visibility check says the config is not visible, don't render it
|
||||
if (visibility && !visibility.call(config, this.props, controlData)) {
|
||||
@@ -160,30 +146,42 @@ class ControlPanelsContainer extends React.Component {
|
||||
name={name}
|
||||
validationErrors={validationErrors}
|
||||
actions={actions}
|
||||
formData={provideFormDataToProps ? formData : null}
|
||||
datasource={formData?.datasource}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderControlPanelSection(section) {
|
||||
renderControlPanelSection(section: ExpandedControlPanelSectionConfig) {
|
||||
const { controls } = this.props;
|
||||
const { label, description } = section;
|
||||
|
||||
// Section label can be a ReactNode but in some places we want to
|
||||
// have a string ID. Using forced type conversion for now,
|
||||
// should probably add a `id` field to sections in the future.
|
||||
const sectionId = String(label);
|
||||
|
||||
const hasErrors = section.controlSetRows.some(rows =>
|
||||
rows.some(
|
||||
s =>
|
||||
controls[s] &&
|
||||
controls[s].validationErrors &&
|
||||
controls[s].validationErrors.length > 0,
|
||||
),
|
||||
rows.some(item => {
|
||||
const controlName =
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: item && 'name' in item
|
||||
? item.name
|
||||
: null;
|
||||
return (
|
||||
controlName &&
|
||||
controlName in controls &&
|
||||
controls[controlName].validationErrors &&
|
||||
controls[controlName].validationErrors.length > 0
|
||||
);
|
||||
}),
|
||||
);
|
||||
const PanelHeader = () => (
|
||||
<span>
|
||||
<span>{label}</span>{' '}
|
||||
{description && (
|
||||
<InfoTooltipWithTrigger label={label} tooltip={description} />
|
||||
// label is only used in tooltip id (should probably call this prop `id`)
|
||||
<InfoTooltipWithTrigger label={sectionId} tooltip={description} />
|
||||
)}
|
||||
{hasErrors && (
|
||||
<InfoTooltipWithTrigger
|
||||
@@ -199,7 +197,7 @@ class ControlPanelsContainer extends React.Component {
|
||||
<Collapse.Panel
|
||||
className="control-panel-section"
|
||||
header={PanelHeader()}
|
||||
key={section.label}
|
||||
key={sectionId}
|
||||
>
|
||||
{section.controlSetRows.map((controlSets, i) => {
|
||||
const renderedControls = controlSets
|
||||
@@ -229,7 +227,6 @@ class ControlPanelsContainer extends React.Component {
|
||||
return (
|
||||
<ControlRow
|
||||
key={`controlsetrow-${i}`}
|
||||
className="control-row"
|
||||
controls={renderedControls}
|
||||
/>
|
||||
);
|
||||
@@ -247,8 +244,8 @@ class ControlPanelsContainer extends React.Component {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const querySectionsToRender = [];
|
||||
const displaySectionsToRender = [];
|
||||
const querySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
|
||||
const displaySectionsToRender: ExpandedControlPanelSectionConfig[] = [];
|
||||
this.sectionsToRender().forEach(section => {
|
||||
// if at least one control in the section is not `renderTrigger`
|
||||
// or asks to be displayed at the Data tab
|
||||
@@ -258,6 +255,8 @@ class ControlPanelsContainer extends React.Component {
|
||||
rows.some(
|
||||
control =>
|
||||
control &&
|
||||
typeof control === 'object' &&
|
||||
'config' in control &&
|
||||
control.config &&
|
||||
(!control.config.renderTrigger ||
|
||||
control.config.tabOverride === 'data'),
|
||||
@@ -277,14 +276,6 @@ class ControlPanelsContainer extends React.Component {
|
||||
);
|
||||
return (
|
||||
<Styles>
|
||||
{this.props.alert && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={this.props.alert}
|
||||
closable
|
||||
onClose={this.removeAlert}
|
||||
/>
|
||||
)}
|
||||
<ControlPanelsTabs
|
||||
id="controlSections"
|
||||
data-test="control-tabs"
|
||||
@@ -318,26 +309,19 @@ class ControlPanelsContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ControlPanelsContainer.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps({ explore }) {
|
||||
return {
|
||||
alert: explore.controlPanelAlert,
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
controls: explore.controls,
|
||||
exploreState: explore,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(exploreActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export { ControlPanelsContainer };
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
function mapStateToProps({ explore }: ExploreState) {
|
||||
return {
|
||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||
controls: explore.controls,
|
||||
exploreState: explore,
|
||||
};
|
||||
},
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(exploreActions, dispatch),
|
||||
};
|
||||
},
|
||||
)(ControlPanelsContainer);
|
||||
@@ -16,20 +16,20 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ExpandedControlItem } from '@superset-ui/chart-controls';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const NUM_COLUMNS = 12;
|
||||
|
||||
const propTypes = {
|
||||
controls: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
function ControlSetRow(props) {
|
||||
const colSize = NUM_COLUMNS / props.controls.length;
|
||||
export default function ControlRow({
|
||||
controls,
|
||||
}: {
|
||||
controls: ExpandedControlItem[];
|
||||
}) {
|
||||
const colSize = NUM_COLUMNS / controls.length;
|
||||
return (
|
||||
<div className="row space-1">
|
||||
{props.controls.map((control, i) => (
|
||||
{controls.map((control, i) => (
|
||||
<div className={`col-lg-${colSize} col-xs-12`} key={i}>
|
||||
{control}
|
||||
</div>
|
||||
@@ -37,6 +37,3 @@ function ControlSetRow(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ControlSetRow.propTypes = propTypes;
|
||||
export default ControlSetRow;
|
||||
@@ -222,10 +222,7 @@ function ExploreViewContainer(props) {
|
||||
}
|
||||
|
||||
function onQuery() {
|
||||
// remove alerts when query
|
||||
props.actions.removeControlPanelAlert();
|
||||
props.actions.triggerQuery(true, props.chart.id);
|
||||
|
||||
addHistory();
|
||||
}
|
||||
|
||||
|
||||
@@ -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