You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by yj...@apache.org on 2020/12/25 04:47:13 UTC

[incubator-superset] branch master updated: feat: Implement drag & drop for metrics and filters labels (#12184)

This is an automated email from the ASF dual-hosted git repository.

yjc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new f3ab1f4  feat: Implement drag & drop for metrics and filters labels (#12184)
f3ab1f4 is described below

commit f3ab1f41eebcd8dfe0379e81dd7083f1a9efa52f
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Fri Dec 25 05:46:37 2020 +0100

    feat: Implement drag & drop for metrics and filters labels (#12184)
---
 .../explore/components/AdhocFilterControl_spec.jsx |   2 +-
 .../explore/components/AdhocFilterOption_spec.jsx  |   2 +-
 .../explore/components/AdhocMetricOption_spec.jsx  |   5 +-
 .../explore/components/MetricsControl_spec.jsx     |   2 +-
 .../components/withAsyncVerification_spec.tsx      |   4 +-
 .../src/explore/DndContextProvider.js              |  23 ++++
 .../src/explore/components/AdhocFilterOption.jsx   |  15 ++-
 .../src/explore/components/AdhocMetricOption.jsx   |  15 ++-
 .../explore/components/MetricDefinitionValue.jsx   |  23 +++-
 .../src/explore/components/OptionControls.tsx      | 122 ++++++++++++++++++++-
 .../components/controls/AdhocFilterControl.jsx     |  19 +++-
 .../explore/components/controls/MetricsControl.jsx |  19 +++-
 .../src/explore/components/optionTypes.ts          |  22 ++++
 13 files changed, 251 insertions(+), 22 deletions(-)

diff --git a/superset-frontend/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx
index 462b651..94af81c 100644
--- a/superset-frontend/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx
@@ -71,7 +71,7 @@ function setup(overrides) {
     ...overrides,
   };
   const wrapper = shallow(<AdhocFilterControl {...props} />);
-  const component = wrapper.dive().shallow();
+  const component = wrapper.dive().dive().shallow();
   return { wrapper, component, onChange };
 }
 
diff --git a/superset-frontend/spec/javascripts/explore/components/AdhocFilterOption_spec.jsx b/superset-frontend/spec/javascripts/explore/components/AdhocFilterOption_spec.jsx
index 56f6436..781f221 100644
--- a/superset-frontend/spec/javascripts/explore/components/AdhocFilterOption_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/AdhocFilterOption_spec.jsx
@@ -56,6 +56,6 @@ describe('AdhocFilterOption', () => {
     const popover = overlay.find(Popover);
     expect(popover).toHaveLength(1);
     expect(popover.props().defaultVisible).toBe(false);
-    expect(overlay.find('OptionControlLabel')).toExist();
+    expect(overlay.find('DraggableOptionControlLabel')).toExist();
   });
 });
diff --git a/superset-frontend/spec/javascripts/explore/components/AdhocMetricOption_spec.jsx b/superset-frontend/spec/javascripts/explore/components/AdhocMetricOption_spec.jsx
index af2fa1f..ceef77d 100644
--- a/superset-frontend/spec/javascripts/explore/components/AdhocMetricOption_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/AdhocMetricOption_spec.jsx
@@ -45,6 +45,9 @@ function setup(overrides) {
     savedMetrics: [],
     onMetricEdit,
     columns,
+    onMoveLabel: () => {},
+    onDropLabel: () => {},
+    index: 0,
     ...overrides,
   };
   const wrapper = shallow(<AdhocMetricOption {...props} />)
@@ -57,7 +60,7 @@ describe('AdhocMetricOption', () => {
   it('renders an overlay trigger wrapper for the label', () => {
     const { wrapper } = setup();
     expect(wrapper.find(Popover)).toExist();
-    expect(wrapper.find('OptionControlLabel')).toExist();
+    expect(wrapper.find('DraggableOptionControlLabel')).toExist();
   });
 
   it('overwrites the adhocMetric in state with onLabelChange', () => {
diff --git a/superset-frontend/spec/javascripts/explore/components/MetricsControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/MetricsControl_spec.jsx
index f21427d..638a20e 100644
--- a/superset-frontend/spec/javascripts/explore/components/MetricsControl_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/MetricsControl_spec.jsx
@@ -53,7 +53,7 @@ function setup(overrides) {
     ...overrides,
   };
   const wrapper = shallow(<MetricsControl {...props} />);
-  const component = wrapper.dive().shallow();
+  const component = wrapper.dive().dive().shallow();
   return { wrapper, component, onChange };
 }
 
diff --git a/superset-frontend/spec/javascripts/explore/components/withAsyncVerification_spec.tsx b/superset-frontend/spec/javascripts/explore/components/withAsyncVerification_spec.tsx
index 44a0333..7d0d259 100644
--- a/superset-frontend/spec/javascripts/explore/components/withAsyncVerification_spec.tsx
+++ b/superset-frontend/spec/javascripts/explore/components/withAsyncVerification_spec.tsx
@@ -120,7 +120,9 @@ describe('VerifiedMetricsControl', () => {
       onChange: mockOnChange,
     });
 
-    const child = wrapper.find(MetricsControl);
+    const child = wrapper.find(MetricsControl) as ReactWrapper<{
+      onChange: (str: string[]) => void;
+    }>;
     child.props().onChange(['abc']);
 
     expect(child.length).toBe(1);
diff --git a/superset-frontend/src/explore/DndContextProvider.js b/superset-frontend/src/explore/DndContextProvider.js
new file mode 100644
index 0000000..884807e
--- /dev/null
+++ b/superset-frontend/src/explore/DndContextProvider.js
@@ -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.
+ */
+// TODO: convert to .ts after we upgrade react-dnd
+import { DragDropContext } from 'react-dnd';
+import HTML5Backend from 'react-dnd-html5-backend';
+
+export default DragDropContext(HTML5Backend);
diff --git a/superset-frontend/src/explore/components/AdhocFilterOption.jsx b/superset-frontend/src/explore/components/AdhocFilterOption.jsx
index 771eebc..a6b26c5 100644
--- a/superset-frontend/src/explore/components/AdhocFilterOption.jsx
+++ b/superset-frontend/src/explore/components/AdhocFilterOption.jsx
@@ -22,7 +22,8 @@ import AdhocFilter from '../AdhocFilter';
 import columnType from '../propTypes/columnType';
 import adhocMetricType from '../propTypes/adhocMetricType';
 import AdhocFilterPopoverTrigger from './AdhocFilterPopoverTrigger';
-import { OptionControlLabel } from './OptionControls';
+import { DraggableOptionControlLabel } from './OptionControls';
+import { OPTION_TYPES } from './optionTypes';
 
 const propTypes = {
   adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
@@ -37,6 +38,9 @@ const propTypes = {
   ).isRequired,
   datasource: PropTypes.object,
   partitionColumn: PropTypes.string,
+  onMoveLabel: PropTypes.func,
+  onDropLabel: PropTypes.func,
+  index: PropTypes.number,
 };
 
 const AdhocFilterOption = ({
@@ -46,6 +50,9 @@ const AdhocFilterOption = ({
   onFilterEdit,
   onRemoveFilter,
   partitionColumn,
+  onMoveLabel,
+  onDropLabel,
+  index,
 }) => (
   <AdhocFilterPopoverTrigger
     adhocFilter={adhocFilter}
@@ -54,9 +61,13 @@ const AdhocFilterOption = ({
     onFilterEdit={onFilterEdit}
     partitionColumn={partitionColumn}
   >
-    <OptionControlLabel
+    <DraggableOptionControlLabel
       label={adhocFilter.getDefaultLabel()}
       onRemove={onRemoveFilter}
+      onMoveLabel={onMoveLabel}
+      onDropLabel={onDropLabel}
+      index={index}
+      type={OPTION_TYPES.filter}
       isAdhoc
     />
   </AdhocFilterPopoverTrigger>
diff --git a/superset-frontend/src/explore/components/AdhocMetricOption.jsx b/superset-frontend/src/explore/components/AdhocMetricOption.jsx
index 9322a97..270db91 100644
--- a/superset-frontend/src/explore/components/AdhocMetricOption.jsx
+++ b/superset-frontend/src/explore/components/AdhocMetricOption.jsx
@@ -21,8 +21,9 @@ import PropTypes from 'prop-types';
 import AdhocMetric from '../AdhocMetric';
 import columnType from '../propTypes/columnType';
 import savedMetricType from '../propTypes/savedMetricType';
-import { OptionControlLabel } from './OptionControls';
+import { DraggableOptionControlLabel } from './OptionControls';
 import AdhocMetricPopoverTrigger from './AdhocMetricPopoverTrigger';
+import { OPTION_TYPES } from './optionTypes';
 
 const propTypes = {
   adhocMetric: PropTypes.instanceOf(AdhocMetric),
@@ -32,6 +33,9 @@ const propTypes = {
   savedMetrics: PropTypes.arrayOf(savedMetricType),
   savedMetric: savedMetricType,
   datasourceType: PropTypes.string,
+  onMoveLabel: PropTypes.func,
+  onDropLabel: PropTypes.func,
+  index: PropTypes.number,
 };
 
 class AdhocMetricOption extends React.PureComponent {
@@ -53,6 +57,9 @@ class AdhocMetricOption extends React.PureComponent {
       savedMetrics,
       savedMetric,
       datasourceType,
+      onMoveLabel,
+      onDropLabel,
+      index,
     } = this.props;
     return (
       <AdhocMetricPopoverTrigger
@@ -63,10 +70,14 @@ class AdhocMetricOption extends React.PureComponent {
         savedMetric={savedMetric}
         datasourceType={datasourceType}
       >
-        <OptionControlLabel
+        <DraggableOptionControlLabel
           savedMetric={savedMetric}
           label={adhocMetric.label}
           onRemove={this.onRemoveMetric}
+          onMoveLabel={onMoveLabel}
+          onDropLabel={onDropLabel}
+          index={index}
+          type={OPTION_TYPES.metric}
           isAdhoc
           isFunction
         />
diff --git a/superset-frontend/src/explore/components/MetricDefinitionValue.jsx b/superset-frontend/src/explore/components/MetricDefinitionValue.jsx
index c2ef35c..1a65da1 100644
--- a/superset-frontend/src/explore/components/MetricDefinitionValue.jsx
+++ b/superset-frontend/src/explore/components/MetricDefinitionValue.jsx
@@ -18,18 +18,21 @@
  */
 import React from 'react';
 import PropTypes from 'prop-types';
-
 import AdhocMetricOption from './AdhocMetricOption';
 import AdhocMetric from '../AdhocMetric';
 import columnType from '../propTypes/columnType';
 import savedMetricType from '../propTypes/savedMetricType';
 import adhocMetricType from '../propTypes/adhocMetricType';
-import { OptionControlLabel } from './OptionControls';
+import { DraggableOptionControlLabel } from './OptionControls';
+import { OPTION_TYPES } from './optionTypes';
 
 const propTypes = {
   option: PropTypes.oneOfType([savedMetricType, adhocMetricType]).isRequired,
+  index: PropTypes.number.isRequired,
   onMetricEdit: PropTypes.func,
   onRemoveMetric: PropTypes.func,
+  onMoveLabel: PropTypes.func,
+  onDropLabel: PropTypes.func,
   columns: PropTypes.arrayOf(columnType),
   savedMetrics: PropTypes.arrayOf(savedMetricType),
   multi: PropTypes.bool,
@@ -43,6 +46,9 @@ export default function MetricDefinitionValue({
   columns,
   savedMetrics,
   datasourceType,
+  onMoveLabel,
+  onDropLabel,
+  index,
 }) {
   const getSavedMetricByName = metricName =>
     savedMetrics.find(metric => metric.metric_name === metricName);
@@ -65,6 +71,9 @@ export default function MetricDefinitionValue({
       savedMetrics,
       datasourceType,
       adhocMetric,
+      onMoveLabel,
+      onDropLabel,
+      index,
       savedMetric: savedMetric ?? {},
     };
 
@@ -72,7 +81,15 @@ export default function MetricDefinitionValue({
   }
   if (typeof option === 'string') {
     return (
-      <OptionControlLabel label={option} onRemove={onRemoveMetric} isFunction />
+      <DraggableOptionControlLabel
+        label={option}
+        onRemove={onRemoveMetric}
+        onMoveLabel={onMoveLabel}
+        onDropLabel={onDropLabel}
+        type={OPTION_TYPES.metric}
+        index={index}
+        isFunction
+      />
     );
   }
   return null;
diff --git a/superset-frontend/src/explore/components/OptionControls.tsx b/superset-frontend/src/explore/components/OptionControls.tsx
index a894d31..31de6ae 100644
--- a/superset-frontend/src/explore/components/OptionControls.tsx
+++ b/superset-frontend/src/explore/components/OptionControls.tsx
@@ -17,12 +17,28 @@
  * under the License.
  */
 import React from 'react';
+import { findDOMNode } from 'react-dom';
+// Current version of react-dnd (2.5.4) doesn't work well with typescript
+// TODO: remove ts-ignore after we upgrade react-dnd
+// @ts-ignore
+import { DragSource, DropTarget } from 'react-dnd';
 import { styled, useTheme } from '@superset-ui/core';
 import { ColumnOption } from '@superset-ui/chart-controls';
 import Icon from '../../components/Icon';
 import { savedMetricType } from '../types';
 
-const OptionControlContainer = styled.div<{ isAdhoc?: boolean }>`
+const TYPE = 'label-dnd';
+
+const DragContainer = styled.div`
+  margin-bottom: ${({ theme }) => theme.gridUnit}px;
+  :last-child {
+    margin-bottom: 0;
+  }
+`;
+
+const OptionControlContainer = styled.div<{
+  isAdhoc?: boolean;
+}>`
   display: flex;
   align-items: center;
   width: 100%;
@@ -31,10 +47,6 @@ const OptionControlContainer = styled.div<{ isAdhoc?: boolean }>`
   background-color: ${({ theme }) => theme.colors.grayscale.light3};
   border-radius: 3px;
   cursor: ${({ isAdhoc }) => (isAdhoc ? 'pointer' : 'default')};
-  margin-bottom: ${({ theme }) => theme.gridUnit}px;
-  :last-child {
-    margin-bottom: 0;
-  }
 `;
 
 const Label = styled.div`
@@ -113,12 +125,81 @@ export const AddIconButton = styled.button`
   }
 `;
 
+const labelSource = {
+  beginDrag({ index, type }: { index: number; type: string }) {
+    return {
+      index,
+      type,
+    };
+  },
+};
+
+const labelTarget = {
+  hover(props: Record<string, any>, monitor: any, component: any) {
+    const { index: dragIndex, type: dragType } = monitor.getItem();
+    const { index: hoverIndex, type: hoverType } = props;
+
+    // Don't replace items with themselves
+    // Don't allow to drag items between filters and metrics boxes
+    if (dragIndex === hoverIndex || dragType !== hoverType) {
+      return;
+    }
+
+    // Determine rectangle on screen
+    // TODO: refactor with references when we upgrade react-dnd
+    // For now we disable warnings about findDOMNode, but we should refactor after we upgrade react-dnd
+    // Current version (2.5.4) doesn't work well with refs
+    // @ts-ignore
+    // eslint-disable-next-line react/no-find-dom-node
+    const hoverBoundingRect = findDOMNode(component)?.getBoundingClientRect();
+
+    // Get vertical middle
+    const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
+
+    // Determine mouse position
+    const clientOffset = monitor.getClientOffset();
+
+    // Get pixels to the top
+    const hoverClientY = clientOffset.y - hoverBoundingRect.top;
+
+    // Only perform the move when the mouse has crossed half of the items height
+    // When dragging downwards, only move when the cursor is below 50%
+    // When dragging upwards, only move when the cursor is above 50%
+
+    // Dragging downwards
+    if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
+      return;
+    }
+
+    // Dragging upwards
+    if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
+      return;
+    }
+
+    // Time to actually perform the action
+    props.onMoveLabel?.(dragIndex, hoverIndex);
+
+    // Note: we're mutating the monitor item here!
+    // Generally it's better to avoid mutations,
+    // but it's good here for the sake of performance
+    // to avoid expensive index searches.
+    // eslint-disable-next-line no-param-reassign
+    monitor.getItem().index = hoverIndex;
+  },
+  drop(props: Record<string, any>) {
+    return props.onDropLabel?.();
+  },
+};
+
 export const OptionControlLabel = ({
   label,
   savedMetric,
   onRemove,
   isAdhoc,
   isFunction,
+  isDraggable,
+  connectDragSource,
+  connectDropTarget,
   ...props
 }: {
   label: string | React.ReactNode;
@@ -126,6 +207,9 @@ export const OptionControlLabel = ({
   onRemove: () => void;
   isAdhoc?: boolean;
   isFunction?: boolean;
+  isDraggable?: boolean;
+  connectDragSource?: any;
+  connectDropTarget?: any;
 }) => {
   const theme = useTheme();
   const getLabelContent = () => {
@@ -139,7 +223,8 @@ export const OptionControlLabel = ({
     }
     return label;
   };
-  return (
+
+  const getOptionControlContent = () => (
     <OptionControlContainer
       isAdhoc={isAdhoc}
       data-test="option-label"
@@ -163,4 +248,29 @@ export const OptionControlLabel = ({
       )}
     </OptionControlContainer>
   );
+
+  return (
+    <DragContainer>
+      {isDraggable
+        ? connectDragSource(
+            connectDropTarget(<div>{getOptionControlContent()}</div>),
+          )
+        : getOptionControlContent()}
+    </DragContainer>
+  );
 };
+
+export const DraggableOptionControlLabel = DropTarget(
+  TYPE,
+  labelTarget,
+  (connect: any) => ({
+    connectDropTarget: connect.dropTarget(),
+  }),
+)(
+  DragSource(TYPE, labelSource, (connect: any) => ({
+    connectDragSource: connect.dragSource(),
+    isDraggable: true,
+  }))(OptionControlLabel),
+);
+
+DraggableOptionControlLabel.displayName = 'DraggableOptionControlLabel';
diff --git a/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx b/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx
index 613c7f7..91f5725 100644
--- a/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx
+++ b/superset-frontend/src/explore/components/controls/AdhocFilterControl.jsx
@@ -18,7 +18,6 @@
  */
 import React from 'react';
 import PropTypes from 'prop-types';
-
 import { t, logging, SupersetClient, withTheme } from '@superset-ui/core';
 
 import ControlHeader from '../ControlHeader';
@@ -39,6 +38,7 @@ import {
 } from '../OptionControls';
 import Icon from '../../../components/Icon';
 import AdhocFilterPopoverTrigger from '../AdhocFilterPopoverTrigger';
+import DndWithHTML5Backend from '../../DndContextProvider';
 
 const propTypes = {
   name: PropTypes.string,
@@ -75,6 +75,7 @@ class AdhocFilterControl extends React.Component {
     this.onRemoveFilter = this.onRemoveFilter.bind(this);
     this.onNewFilter = this.onNewFilter.bind(this);
     this.onFilterEdit = this.onFilterEdit.bind(this);
+    this.moveLabel = this.moveLabel.bind(this);
     this.onChange = this.onChange.bind(this);
     this.getMetricExpression = this.getMetricExpression.bind(this);
 
@@ -86,11 +87,14 @@ class AdhocFilterControl extends React.Component {
     this.valueRenderer = (adhocFilter, index) => (
       <AdhocFilterOption
         key={index}
+        index={index}
         adhocFilter={adhocFilter}
         onFilterEdit={this.onFilterEdit}
         options={this.state.options}
         datasource={this.props.datasource}
         onRemoveFilter={() => this.onRemoveFilter(index)}
+        onMoveLabel={this.moveLabel}
+        onDropLabel={() => this.props.onChange(this.state.values)}
       />
     );
     this.state = {
@@ -256,6 +260,17 @@ class AdhocFilterControl extends React.Component {
     ).expression;
   }
 
+  moveLabel(dragIndex, hoverIndex) {
+    const { values } = this.state;
+
+    const newValues = [...values];
+    [newValues[hoverIndex], newValues[dragIndex]] = [
+      newValues[dragIndex],
+      newValues[hoverIndex],
+    ];
+    this.setState({ values: newValues });
+  }
+
   optionsForSelect(props) {
     const options = [
       ...props.columns,
@@ -349,4 +364,4 @@ class AdhocFilterControl extends React.Component {
 AdhocFilterControl.propTypes = propTypes;
 AdhocFilterControl.defaultProps = defaultProps;
 
-export default withTheme(AdhocFilterControl);
+export default DndWithHTML5Backend(withTheme(AdhocFilterControl));
diff --git a/superset-frontend/src/explore/components/controls/MetricsControl.jsx b/superset-frontend/src/explore/components/controls/MetricsControl.jsx
index 1bc88c6..f643e2b 100644
--- a/superset-frontend/src/explore/components/controls/MetricsControl.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricsControl.jsx
@@ -20,7 +20,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { t, withTheme } from '@superset-ui/core';
 import { isEqual } from 'lodash';
-
 import ControlHeader from '../ControlHeader';
 import MetricDefinitionOption from '../MetricDefinitionOption';
 import MetricDefinitionValue from '../MetricDefinitionValue';
@@ -41,6 +40,7 @@ import {
   HeaderContainer,
   LabelsContainer,
 } from '../OptionControls';
+import DndWithHTML5Backend from '../../DndContextProvider';
 
 const propTypes = {
   name: PropTypes.string.isRequired,
@@ -116,6 +116,7 @@ class MetricsControl extends React.PureComponent {
     this.onMetricEdit = this.onMetricEdit.bind(this);
     this.onNewMetric = this.onNewMetric.bind(this);
     this.onRemoveMetric = this.onRemoveMetric.bind(this);
+    this.moveLabel = this.moveLabel.bind(this);
     this.checkIfAggregateInInput = this.checkIfAggregateInInput.bind(this);
     this.optionsForSelect = this.optionsForSelect.bind(this);
     this.selectFilterOption = this.selectFilterOption.bind(this);
@@ -124,12 +125,15 @@ class MetricsControl extends React.PureComponent {
     this.valueRenderer = (option, index) => (
       <MetricDefinitionValue
         key={index}
+        index={index}
         option={option}
         onMetricEdit={this.onMetricEdit}
         onRemoveMetric={() => this.onRemoveMetric(index)}
         columns={this.props.columns}
         savedMetrics={this.props.savedMetrics}
         datasourceType={this.props.datasourceType}
+        onMoveLabel={this.moveLabel}
+        onDropLabel={() => this.props.onChange(this.state.value)}
       />
     );
     this.select = null;
@@ -238,6 +242,17 @@ class MetricsControl extends React.PureComponent {
     this.props.onChange(this.props.multi ? optionValues : optionValues[0]);
   }
 
+  moveLabel(dragIndex, hoverIndex) {
+    const { value } = this.state;
+
+    const newValues = [...value];
+    [newValues[hoverIndex], newValues[dragIndex]] = [
+      newValues[dragIndex],
+      newValues[hoverIndex],
+    ];
+    this.setState({ value: newValues });
+  }
+
   isAddNewMetricDisabled() {
     return !this.props.multi && this.state.value.length > 0;
   }
@@ -377,4 +392,4 @@ class MetricsControl extends React.PureComponent {
 MetricsControl.propTypes = propTypes;
 MetricsControl.defaultProps = defaultProps;
 
-export default withTheme(MetricsControl);
+export default DndWithHTML5Backend(withTheme(MetricsControl));
diff --git a/superset-frontend/src/explore/components/optionTypes.ts b/superset-frontend/src/explore/components/optionTypes.ts
new file mode 100644
index 0000000..4d86a8f
--- /dev/null
+++ b/superset-frontend/src/explore/components/optionTypes.ts
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+export const OPTION_TYPES = {
+  metric: 'metric',
+  filter: 'filter',
+};