You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2021/08/02 13:23:25 UTC

[superset] branch master updated: fix(explore): fix undefined error when using dnd (#16020)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 3061b6a  fix(explore): fix undefined error when using dnd (#16020)
3061b6a is described below

commit 3061b6ad09606c6e1e431c196b3026da8465ba9e
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Mon Aug 2 15:22:32 2021 +0200

    fix(explore): fix undefined error when using dnd (#16020)
---
 .../DndColumnSelectControl/DndColumnSelect.tsx     | 103 ++++---
 .../DndColumnSelectControl/DndFilterSelect.tsx     | 308 ++++++++++++---------
 .../DndColumnSelectControl/DndMetricSelect.tsx     | 214 ++++++++------
 .../controls/DndColumnSelectControl/Option.tsx     |   8 +-
 .../DndColumnSelectControl/OptionWrapper.tsx       |  16 +-
 .../controls/DndColumnSelectControl/types.ts       |   1 +
 .../controls/MetricControl/AdhocMetricOption.jsx   |   6 +-
 .../MetricControl/MetricDefinitionValue.jsx        |   2 +
 .../controls/MetricControl/MetricsControl.jsx      |   2 +-
 9 files changed, 389 insertions(+), 271 deletions(-)

diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx
index d677aa2..839784f 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
 import { tn } from '@superset-ui/core';
 import { ColumnMeta } from '@superset-ui/chart-controls';
 import { isEmpty } from 'lodash';
@@ -36,8 +36,13 @@ export const DndColumnSelect = (props: LabelProps) => {
     onChange,
     canDelete = true,
     ghostButtonText,
+    name,
+    label,
   } = props;
-  const optionSelector = new OptionSelector(options, multi, value);
+  const optionSelector = useMemo(
+    () => new OptionSelector(options, multi, value),
+    [multi, options, value],
+  );
 
   // synchronize values in case of dataset changes
   useEffect(() => {
@@ -63,46 +68,68 @@ export const DndColumnSelect = (props: LabelProps) => {
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [JSON.stringify(value), JSON.stringify(optionSelector.getValues())]);
 
-  const onDrop = (item: DatasourcePanelDndItem) => {
-    const column = item.value as ColumnMeta;
-    if (!optionSelector.multi && !isEmpty(optionSelector.values)) {
-      optionSelector.replace(0, column.column_name);
-    } else {
-      optionSelector.add(column.column_name);
-    }
-    onChange(optionSelector.getValues());
-  };
+  const onDrop = useCallback(
+    (item: DatasourcePanelDndItem) => {
+      const column = item.value as ColumnMeta;
+      if (!optionSelector.multi && !isEmpty(optionSelector.values)) {
+        optionSelector.replace(0, column.column_name);
+      } else {
+        optionSelector.add(column.column_name);
+      }
+      onChange(optionSelector.getValues());
+    },
+    [onChange, optionSelector],
+  );
 
-  const canDrop = (item: DatasourcePanelDndItem) => {
-    const columnName = (item.value as ColumnMeta).column_name;
-    return (
-      columnName in optionSelector.options && !optionSelector.has(columnName)
-    );
-  };
+  const canDrop = useCallback(
+    (item: DatasourcePanelDndItem) => {
+      const columnName = (item.value as ColumnMeta).column_name;
+      return (
+        columnName in optionSelector.options && !optionSelector.has(columnName)
+      );
+    },
+    [optionSelector],
+  );
 
-  const onClickClose = (index: number) => {
-    optionSelector.del(index);
-    onChange(optionSelector.getValues());
-  };
+  const onClickClose = useCallback(
+    (index: number) => {
+      optionSelector.del(index);
+      onChange(optionSelector.getValues());
+    },
+    [onChange, optionSelector],
+  );
 
-  const onShiftOptions = (dragIndex: number, hoverIndex: number) => {
-    optionSelector.swap(dragIndex, hoverIndex);
-    onChange(optionSelector.getValues());
-  };
+  const onShiftOptions = useCallback(
+    (dragIndex: number, hoverIndex: number) => {
+      optionSelector.swap(dragIndex, hoverIndex);
+      onChange(optionSelector.getValues());
+    },
+    [onChange, optionSelector],
+  );
 
-  const valuesRenderer = () =>
-    optionSelector.values.map((column, idx) => (
-      <OptionWrapper
-        key={idx}
-        index={idx}
-        clickClose={onClickClose}
-        onShiftOptions={onShiftOptions}
-        type={DndItemType.ColumnOption}
-        canDelete={canDelete}
-      >
-        <StyledColumnOption column={column} showType />
-      </OptionWrapper>
-    ));
+  const valuesRenderer = useCallback(
+    () =>
+      optionSelector.values.map((column, idx) => (
+        <OptionWrapper
+          key={idx}
+          index={idx}
+          clickClose={onClickClose}
+          onShiftOptions={onShiftOptions}
+          type={`${DndItemType.ColumnOption}_${name}_${label}`}
+          canDelete={canDelete}
+        >
+          <StyledColumnOption column={column} showType />
+        </OptionWrapper>
+      )),
+    [
+      canDelete,
+      label,
+      name,
+      onClickClose,
+      onShiftOptions,
+      optionSelector.values,
+    ],
+  );
 
   return (
     <DndSelectLabel<string | string[], ColumnMeta[]>
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
index f0b7c1d..dc7e892 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import {
   FeatureFlag,
   isFeatureEnabled,
@@ -50,10 +50,19 @@ import {
 } from 'src/explore/components/DatasourcePanel/types';
 import { DndItemType } from 'src/explore/components/DndItemType';
 
+const DND_ACCEPTED_TYPES = [
+  DndItemType.Column,
+  DndItemType.Metric,
+  DndItemType.MetricOption,
+  DndItemType.AdhocMetricOption,
+];
+
 const isDictionaryForAdhocFilter = (value: OptionValueType) =>
   !(value instanceof AdhocFilter) && value?.expressionType;
 
 export const DndFilterSelect = (props: DndFilterSelectProps) => {
+  const { datasource, onChange } = props;
+
   const propsValues = Array.from(props.value ?? []);
   const [values, setValues] = useState(
     propsValues.map((filter: OptionValueType) =>
@@ -117,7 +126,6 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
   );
 
   useEffect(() => {
-    const { datasource } = props;
     if (datasource && datasource.type === 'table') {
       const dbId = datasource.database?.id;
       const {
@@ -149,7 +157,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
           });
       }
     }
-  }, []);
+  }, [datasource]);
 
   useEffect(() => {
     setOptions(optionsForSelect(props.columns, props.formData));
@@ -163,138 +171,167 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
     );
   }, [props.value]);
 
-  const onClickClose = (index: number) => {
-    const valuesCopy = [...values];
-    valuesCopy.splice(index, 1);
-    setValues(valuesCopy);
-    props.onChange(valuesCopy);
-  };
+  const onClickClose = useCallback(
+    (index: number) => {
+      const valuesCopy = [...values];
+      valuesCopy.splice(index, 1);
+      setValues(valuesCopy);
+      onChange(valuesCopy);
+    },
+    [onChange, values],
+  );
 
-  const onShiftOptions = (dragIndex: number, hoverIndex: number) => {
-    const newValues = [...values];
-    [newValues[hoverIndex], newValues[dragIndex]] = [
-      newValues[dragIndex],
-      newValues[hoverIndex],
-    ];
-    setValues(newValues);
-  };
+  const onShiftOptions = useCallback(
+    (dragIndex: number, hoverIndex: number) => {
+      const newValues = [...values];
+      [newValues[hoverIndex], newValues[dragIndex]] = [
+        newValues[dragIndex],
+        newValues[hoverIndex],
+      ];
+      setValues(newValues);
+    },
+    [values],
+  );
 
-  const getMetricExpression = (savedMetricName: string) =>
-    props.savedMetrics.find(
-      (savedMetric: Metric) => savedMetric.metric_name === savedMetricName,
-    )?.expression;
+  const getMetricExpression = useCallback(
+    (savedMetricName: string) =>
+      props.savedMetrics.find(
+        (savedMetric: Metric) => savedMetric.metric_name === savedMetricName,
+      )?.expression,
+    [props.savedMetrics],
+  );
 
-  const mapOption = (option: OptionValueType) => {
-    // already a AdhocFilter, skip
-    if (option instanceof AdhocFilter) {
-      return option;
-    }
-    const filterOptions = option as Record<string, any>;
-    // via datasource saved metric
-    if (filterOptions.saved_metric_name) {
-      return new AdhocFilter({
-        expressionType:
-          props.datasource.type === 'druid'
-            ? EXPRESSION_TYPES.SIMPLE
-            : EXPRESSION_TYPES.SQL,
-        subject:
-          props.datasource.type === 'druid'
-            ? filterOptions.saved_metric_name
-            : getMetricExpression(filterOptions.saved_metric_name),
-        operator:
-          OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
-        operatorId: Operators.GREATER_THAN,
-        comparator: 0,
-        clause: CLAUSES.HAVING,
-      });
-    }
-    // has a custom label, meaning it's custom column
-    if (filterOptions.label) {
-      return new AdhocFilter({
-        expressionType:
-          props.datasource.type === 'druid'
-            ? EXPRESSION_TYPES.SIMPLE
-            : EXPRESSION_TYPES.SQL,
-        subject:
-          props.datasource.type === 'druid'
-            ? filterOptions.label
-            : new AdhocMetric(option).translateToSql(),
-        operator:
-          OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
-        operatorId: Operators.GREATER_THAN,
-        comparator: 0,
-        clause: CLAUSES.HAVING,
-      });
-    }
-    // add a new filter item
-    if (filterOptions.column_name) {
-      return new AdhocFilter({
-        expressionType: EXPRESSION_TYPES.SIMPLE,
-        subject: filterOptions.column_name,
-        operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.EQUALS].operation,
-        operatorId: Operators.EQUALS,
-        comparator: '',
-        clause: CLAUSES.WHERE,
-        isNew: true,
-      });
-    }
-    return null;
-  };
+  const mapOption = useCallback(
+    (option: OptionValueType) => {
+      // already a AdhocFilter, skip
+      if (option instanceof AdhocFilter) {
+        return option;
+      }
+      const filterOptions = option as Record<string, any>;
+      // via datasource saved metric
+      if (filterOptions.saved_metric_name) {
+        return new AdhocFilter({
+          expressionType:
+            datasource.type === 'druid'
+              ? EXPRESSION_TYPES.SIMPLE
+              : EXPRESSION_TYPES.SQL,
+          subject:
+            datasource.type === 'druid'
+              ? filterOptions.saved_metric_name
+              : getMetricExpression(filterOptions.saved_metric_name),
+          operator:
+            OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
+          operatorId: Operators.GREATER_THAN,
+          comparator: 0,
+          clause: CLAUSES.HAVING,
+        });
+      }
+      // has a custom label, meaning it's custom column
+      if (filterOptions.label) {
+        return new AdhocFilter({
+          expressionType:
+            datasource.type === 'druid'
+              ? EXPRESSION_TYPES.SIMPLE
+              : EXPRESSION_TYPES.SQL,
+          subject:
+            datasource.type === 'druid'
+              ? filterOptions.label
+              : new AdhocMetric(option).translateToSql(),
+          operator:
+            OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.GREATER_THAN].operation,
+          operatorId: Operators.GREATER_THAN,
+          comparator: 0,
+          clause: CLAUSES.HAVING,
+        });
+      }
+      // add a new filter item
+      if (filterOptions.column_name) {
+        return new AdhocFilter({
+          expressionType: EXPRESSION_TYPES.SIMPLE,
+          subject: filterOptions.column_name,
+          operator: OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.EQUALS].operation,
+          operatorId: Operators.EQUALS,
+          comparator: '',
+          clause: CLAUSES.WHERE,
+          isNew: true,
+        });
+      }
+      return null;
+    },
+    [datasource.type, getMetricExpression],
+  );
 
-  const onFilterEdit = (changedFilter: AdhocFilter) => {
-    props.onChange(
-      values.map((value: AdhocFilter) => {
-        if (value.filterOptionName === changedFilter.filterOptionName) {
-          return changedFilter;
-        }
-        return value;
-      }),
-    );
-  };
+  const onFilterEdit = useCallback(
+    (changedFilter: AdhocFilter) => {
+      onChange(
+        values.map((value: AdhocFilter) => {
+          if (value.filterOptionName === changedFilter.filterOptionName) {
+            return changedFilter;
+          }
+          return value;
+        }),
+      );
+    },
+    [onChange, values],
+  );
 
-  const onNewFilter = (newFilter: AdhocFilter) => {
-    const mappedOption = mapOption(newFilter);
-    if (mappedOption) {
-      const newValues = [...values, mappedOption];
-      setValues(newValues);
-      props.onChange(newValues);
-    }
-  };
+  const onNewFilter = useCallback(
+    (newFilter: AdhocFilter) => {
+      const mappedOption = mapOption(newFilter);
+      if (mappedOption) {
+        const newValues = [...values, mappedOption];
+        setValues(newValues);
+        onChange(newValues);
+      }
+    },
+    [mapOption, onChange, values],
+  );
 
-  const togglePopover = (visible: boolean) => {
+  const togglePopover = useCallback((visible: boolean) => {
     setNewFilterPopoverVisible(visible);
-  };
+  }, []);
 
-  const closePopover = () => {
+  const closePopover = useCallback(() => {
     togglePopover(false);
-  };
+  }, [togglePopover]);
 
-  const valuesRenderer = () =>
-    values.map((adhocFilter: AdhocFilter, index: number) => {
-      const label = adhocFilter.getDefaultLabel();
-      return (
-        <AdhocFilterPopoverTrigger
-          key={index}
-          adhocFilter={adhocFilter}
-          options={options}
-          datasource={props.datasource}
-          onFilterEdit={onFilterEdit}
-          partitionColumn={partitionColumn}
-        >
-          <OptionWrapper
+  const valuesRenderer = useCallback(
+    () =>
+      values.map((adhocFilter: AdhocFilter, index: number) => {
+        const label = adhocFilter.getDefaultLabel();
+        return (
+          <AdhocFilterPopoverTrigger
             key={index}
-            index={index}
-            clickClose={onClickClose}
-            onShiftOptions={onShiftOptions}
-            type={DndItemType.FilterOption}
-            withCaret
-            isExtra={adhocFilter.isExtra}
+            adhocFilter={adhocFilter}
+            options={options}
+            datasource={datasource}
+            onFilterEdit={onFilterEdit}
+            partitionColumn={partitionColumn}
           >
-            <Tooltip title={label}>{label}</Tooltip>
-          </OptionWrapper>
-        </AdhocFilterPopoverTrigger>
-      );
-    });
+            <OptionWrapper
+              key={index}
+              index={index}
+              clickClose={onClickClose}
+              onShiftOptions={onShiftOptions}
+              type={DndItemType.FilterOption}
+              withCaret
+              isExtra={adhocFilter.isExtra}
+            >
+              <Tooltip title={label}>{label}</Tooltip>
+            </OptionWrapper>
+          </AdhocFilterPopoverTrigger>
+        );
+      }),
+    [
+      onClickClose,
+      onFilterEdit,
+      onShiftOptions,
+      options,
+      partitionColumn,
+      datasource,
+      values,
+    ],
+  );
 
   const adhocFilter = useMemo(() => {
     if (droppedItem?.metric_name) {
@@ -321,28 +358,29 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
     return new AdhocFilter(config);
   }, [droppedItem]);
 
+  const canDrop = useCallback(() => true, []);
+  const handleDrop = useCallback(
+    (item: DatasourcePanelDndItem) => {
+      setDroppedItem(item.value);
+      togglePopover(true);
+    },
+    [togglePopover],
+  );
+
   return (
     <>
       <DndSelectLabel<OptionValueType, OptionValueType[]>
-        onDrop={(item: DatasourcePanelDndItem) => {
-          setDroppedItem(item.value);
-          togglePopover(true);
-        }}
-        canDrop={() => true}
+        onDrop={handleDrop}
+        canDrop={canDrop}
         valuesRenderer={valuesRenderer}
-        accept={[
-          DndItemType.Column,
-          DndItemType.Metric,
-          DndItemType.MetricOption,
-          DndItemType.AdhocMetricOption,
-        ]}
+        accept={DND_ACCEPTED_TYPES}
         ghostButtonText={t('Drop columns or metrics')}
         {...props}
       />
       <AdhocFilterPopoverTrigger
         adhocFilter={adhocFilter}
         options={options}
-        datasource={props.datasource}
+        datasource={datasource}
         onFilterEdit={onNewFilter}
         partitionColumn={partitionColumn}
         isControlledComponent
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
index 9e4efd5..77dbfa3 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
@@ -39,6 +39,9 @@ import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectContr
 import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
 import { AGGREGATES } from 'src/explore/constants';
 
+const EMPTY_OBJECT = {};
+const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
+
 const isDictionaryForAdhocMetric = (value: any) =>
   value && !(value instanceof AdhocMetric) && value.expressionType;
 
@@ -162,102 +165,144 @@ export const DndMetricSelect = (props: any) => {
     onChange,
   ]);
 
-  const canDrop = (item: DatasourcePanelDndItem) => {
-    const isMetricAlreadyInValues =
-      item.type === 'metric' ? value.includes(item.value.metric_name) : false;
-    return !isMetricAlreadyInValues;
-  };
+  const canDrop = useCallback(
+    (item: DatasourcePanelDndItem) => {
+      const isMetricAlreadyInValues =
+        item.type === 'metric' ? value.includes(item.value.metric_name) : false;
+      return !isMetricAlreadyInValues;
+    },
+    [value],
+  );
+
+  const onNewMetric = useCallback(
+    (newMetric: Metric) => {
+      const newValue = props.multi ? [...value, newMetric] : [newMetric];
+      setValue(newValue);
+      handleChange(newValue);
+    },
+    [handleChange, props.multi, value],
+  );
 
-  const onNewMetric = (newMetric: Metric) => {
-    const newValue = props.multi ? [...value, newMetric] : [newMetric];
-    setValue(newValue);
-    handleChange(newValue);
-  };
+  const onMetricEdit = useCallback(
+    (changedMetric: Metric | AdhocMetric, oldMetric: Metric | AdhocMetric) => {
+      const newValue = value.map(value => {
+        if (
+          // compare saved metrics
+          value === (oldMetric as Metric).metric_name ||
+          // compare adhoc metrics
+          typeof (value as AdhocMetric).optionName !== 'undefined'
+            ? (value as AdhocMetric).optionName ===
+              (oldMetric as AdhocMetric).optionName
+            : false
+        ) {
+          return changedMetric;
+        }
+        return value;
+      });
+      setValue(newValue);
+      handleChange(newValue);
+    },
+    [handleChange, value],
+  );
 
-  const onMetricEdit = (
-    changedMetric: Metric | AdhocMetric,
-    oldMetric: Metric | AdhocMetric,
-  ) => {
-    const newValue = value.map(value => {
-      if (
-        // compare saved metrics
-        value === (oldMetric as Metric).metric_name ||
-        // compare adhoc metrics
-        typeof (value as AdhocMetric).optionName !== 'undefined'
-          ? (value as AdhocMetric).optionName ===
-            (oldMetric as AdhocMetric).optionName
-          : false
-      ) {
-        return changedMetric;
+  const onRemoveMetric = useCallback(
+    (index: number) => {
+      if (!Array.isArray(value)) {
+        return;
       }
-      return value;
-    });
-    setValue(newValue);
-    handleChange(newValue);
-  };
+      const valuesCopy = [...value];
+      valuesCopy.splice(index, 1);
+      setValue(valuesCopy);
+      onChange(valuesCopy);
+    },
+    [onChange, value],
+  );
 
-  const onRemoveMetric = (index: number) => {
-    if (!Array.isArray(value)) {
-      return;
-    }
-    const valuesCopy = [...value];
-    valuesCopy.splice(index, 1);
-    setValue(valuesCopy);
-    onChange(valuesCopy);
-  };
+  const moveLabel = useCallback(
+    (dragIndex: number, hoverIndex: number) => {
+      const newValues = [...value];
+      [newValues[hoverIndex], newValues[dragIndex]] = [
+        newValues[dragIndex],
+        newValues[hoverIndex],
+      ];
+      setValue(newValues);
+    },
+    [value],
+  );
 
-  const moveLabel = (dragIndex: number, hoverIndex: number) => {
-    const newValues = [...value];
-    [newValues[hoverIndex], newValues[dragIndex]] = [
-      newValues[dragIndex],
-      newValues[hoverIndex],
-    ];
-    setValue(newValues);
-  };
+  const newSavedMetricOptions = useMemo(
+    () => getOptionsForSavedMetrics(props.savedMetrics, props.value),
+    [props.savedMetrics, props.value],
+  );
 
-  const valueRenderer = (
-    option: Metric | AdhocMetric | string,
-    index: number,
-  ) => (
-    <MetricDefinitionValue
-      key={index}
-      index={index}
-      option={option}
-      onMetricEdit={onMetricEdit}
-      onRemoveMetric={() => onRemoveMetric(index)}
-      columns={props.columns}
-      savedMetrics={props.savedMetrics}
-      savedMetricsOptions={getOptionsForSavedMetrics(
+  const getSavedMetricOptionsForMetric = useCallback(
+    index =>
+      getOptionsForSavedMetrics(
         props.savedMetrics,
         props.value,
         props.value?.[index],
-      )}
-      datasourceType={props.datasourceType}
-      onMoveLabel={moveLabel}
-      onDropLabel={() => onChange(value)}
-    />
+      ),
+    [props.savedMetrics, props.value],
+  );
+
+  const handleDropLabel = useCallback(() => onChange(value), [onChange, value]);
+
+  const valueRenderer = useCallback(
+    (option: Metric | AdhocMetric | string, index: number) => (
+      <MetricDefinitionValue
+        key={index}
+        index={index}
+        option={option}
+        onMetricEdit={onMetricEdit}
+        onRemoveMetric={onRemoveMetric}
+        columns={props.columns}
+        savedMetrics={props.savedMetrics}
+        savedMetricsOptions={getSavedMetricOptionsForMetric(index)}
+        datasourceType={props.datasourceType}
+        onMoveLabel={moveLabel}
+        onDropLabel={handleDropLabel}
+        type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
+      />
+    ),
+    [
+      getSavedMetricOptionsForMetric,
+      handleDropLabel,
+      moveLabel,
+      onMetricEdit,
+      onRemoveMetric,
+      props.columns,
+      props.datasourceType,
+      props.label,
+      props.name,
+      props.savedMetrics,
+    ],
   );
 
-  const valuesRenderer = () =>
-    value.map((value, index) => valueRenderer(value, index));
+  const valuesRenderer = useCallback(
+    () => value.map((value, index) => valueRenderer(value, index)),
+    [value, valueRenderer],
+  );
 
-  const togglePopover = (visible: boolean) => {
+  const togglePopover = useCallback((visible: boolean) => {
     setNewMetricPopoverVisible(visible);
-  };
+  }, []);
 
-  const closePopover = () => {
+  const closePopover = useCallback(() => {
     togglePopover(false);
-  };
+  }, [togglePopover]);
 
-  const handleDrop = (item: DatasourcePanelDndItem) => {
-    if (item.type === DndItemType.Metric) {
-      onNewMetric(item.value as Metric);
-    }
-    if (item.type === DndItemType.Column) {
-      setDroppedItem(item);
-      togglePopover(true);
-    }
-  };
+  const handleDrop = useCallback(
+    (item: DatasourcePanelDndItem) => {
+      if (item.type === DndItemType.Metric) {
+        onNewMetric(item.value as Metric);
+      }
+      if (item.type === DndItemType.Column) {
+        setDroppedItem(item);
+        togglePopover(true);
+      }
+    },
+    [onNewMetric, togglePopover],
+  );
 
   const adhocMetric = useMemo(() => {
     if (droppedItem?.type === DndItemType.Column) {
@@ -287,7 +332,7 @@ export const DndMetricSelect = (props: any) => {
         onDrop={handleDrop}
         canDrop={canDrop}
         valuesRenderer={valuesRenderer}
-        accept={[DndItemType.Column, DndItemType.Metric]}
+        accept={DND_ACCEPTED_TYPES}
         ghostButtonText={tn(
           'Drop column or metric',
           'Drop columns or metrics',
@@ -300,11 +345,8 @@ export const DndMetricSelect = (props: any) => {
         adhocMetric={adhocMetric}
         onMetricEdit={onNewMetric}
         columns={props.columns}
-        savedMetricsOptions={getOptionsForSavedMetrics(
-          props.savedMetrics,
-          props.value,
-        )}
-        savedMetric={{} as savedMetricType}
+        savedMetricsOptions={newSavedMetricOptions}
+        savedMetric={EMPTY_OBJECT as savedMetricType}
         datasourceType={props.datasourceType}
         isControlledComponent
         visible={newMetricPopoverVisible}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx
index 50e1bfe..824a658 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
+import React, { useCallback } from 'react';
 import { styled, t, useTheme } from '@superset-ui/core';
 import Icons from 'src/components/Icons';
 import {
@@ -41,13 +41,17 @@ export default function Option({
   canDelete = true,
 }: OptionProps) {
   const theme = useTheme();
+  const onClickClose = useCallback(() => clickClose(index), [
+    clickClose,
+    index,
+  ]);
   return (
     <OptionControlContainer data-test="option-label" withCaret={withCaret}>
       {canDelete && (
         <CloseContainer
           role="button"
           data-test="remove-control-button"
-          onClick={() => clickClose(index)}
+          onClick={onClickClose}
         >
           <Icons.XSmall iconColor={theme.colors.grayscale.light1} />
         </CloseContainer>
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx
index 62230c5..d624817 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useRef } from 'react';
+import React, { useMemo, useRef } from 'react';
 import {
   useDrag,
   useDrop,
@@ -24,7 +24,6 @@ import {
   DragSourceMonitor,
 } from 'react-dnd';
 import { DragContainer } from 'src/explore/components/controls/OptionControls';
-import { DndItemType } from 'src/explore/components/DndItemType';
 import {
   OptionProps,
   OptionItemInterface,
@@ -33,7 +32,7 @@ import Option from './Option';
 
 export default function OptionWrapper(
   props: OptionProps & {
-    type: DndItemType;
+    type: string;
     onShiftOptions: (dragIndex: number, hoverIndex: number) => void;
   },
 ) {
@@ -50,10 +49,13 @@ export default function OptionWrapper(
   } = props;
   const ref = useRef<HTMLDivElement>(null);
 
-  const item: OptionItemInterface = {
-    dragIndex: index,
-    type,
-  };
+  const item: OptionItemInterface = useMemo(
+    () => ({
+      dragIndex: index,
+      type,
+    }),
+    [index, type],
+  );
   const [, drag] = useDrag({
     item,
     collect: (monitor: DragSourceMonitor) => ({
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
index 2d7142e..f11bb1d 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
@@ -44,6 +44,7 @@ export interface LabelProps<T = string[] | string> {
   multi?: boolean;
   canDelete?: boolean;
   ghostButtonText?: string;
+  label?: string;
 }
 
 export interface DndColumnSelectProps<
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
index 9a9fd20..31f749c 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
@@ -36,6 +36,7 @@ const propTypes = {
   onMoveLabel: PropTypes.func,
   onDropLabel: PropTypes.func,
   index: PropTypes.number,
+  type: PropTypes.string,
 };
 
 class AdhocMetricOption extends React.PureComponent {
@@ -46,7 +47,7 @@ class AdhocMetricOption extends React.PureComponent {
 
   onRemoveMetric(e) {
     e.stopPropagation();
-    this.props.onRemoveMetric();
+    this.props.onRemoveMetric(this.props.index);
   }
 
   render() {
@@ -60,6 +61,7 @@ class AdhocMetricOption extends React.PureComponent {
       onMoveLabel,
       onDropLabel,
       index,
+      type,
     } = this.props;
 
     return (
@@ -79,7 +81,7 @@ class AdhocMetricOption extends React.PureComponent {
           onMoveLabel={onMoveLabel}
           onDropLabel={onDropLabel}
           index={index}
-          type={DndItemType.AdhocMetricOption}
+          type={type ?? DndItemType.AdhocMetricOption}
           withCaret
           isFunction
         />
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
index 44a6527..47520c6 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
@@ -48,6 +48,7 @@ export default function MetricDefinitionValue({
   onMoveLabel,
   onDropLabel,
   index,
+  type,
 }) {
   const getSavedMetricByName = metricName =>
     savedMetrics.find(metric => metric.metric_name === metricName);
@@ -74,6 +75,7 @@ export default function MetricDefinitionValue({
       onDropLabel,
       index,
       savedMetric: savedMetric ?? {},
+      type,
     };
 
     return <AdhocMetricOption {...metricOptionProps} />;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
index c8b7205..2e24b26 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
@@ -142,7 +142,7 @@ class MetricsControl extends React.PureComponent {
         index={index}
         option={option}
         onMetricEdit={this.onMetricEdit}
-        onRemoveMetric={() => this.onRemoveMetric(index)}
+        onRemoveMetric={this.onRemoveMetric}
         columns={this.props.columns}
         datasource={this.props.datasource}
         savedMetrics={this.props.savedMetrics}