You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2022/10/19 21:34:54 UTC

[superset] branch master updated: feat(dashboard): menu improvements, fallback support for Drill to Detail (#21351)

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

michaelsmolina 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 76e57ec651 feat(dashboard): menu improvements, fallback support for Drill to Detail (#21351)
76e57ec651 is described below

commit 76e57ec651bbfaf4f76031eeeca66f6a1fa81bc2
Author: Cody Leff <co...@preset.io>
AuthorDate: Wed Oct 19 15:34:46 2022 -0600

    feat(dashboard): menu improvements, fallback support for Drill to Detail (#21351)
---
 .../integration/dashboard/drilltodetail.test.ts    |  42 ++-
 .../superset-ui-core/src/chart/types/Base.ts       |   6 +
 .../packages/superset-ui-core/src/query/index.ts   |   1 +
 .../legacy-plugin-chart-world-map/src/WorldMap.js  |   2 +-
 .../legacy-plugin-chart-world-map/src/index.js     |   3 +-
 .../src/BigNumber/BigNumberTotal/index.ts          |   3 +-
 .../src/BigNumber/BigNumberViz.tsx                 |  14 +-
 .../src/BigNumber/BigNumberWithTrendline/index.ts  |   3 +-
 .../plugin-chart-echarts/src/BoxPlot/index.ts      |   2 +-
 .../plugin-chart-echarts/src/Funnel/index.ts       |   2 +-
 .../plugin-chart-echarts/src/Gauge/index.ts        |   2 +-
 .../src/Graph/EchartsGraph.tsx                     |   6 +-
 .../plugin-chart-echarts/src/Graph/index.ts        |   3 +-
 .../plugin-chart-echarts/src/Graph/types.ts        |   4 +-
 .../src/MixedTimeseries/EchartsMixedTimeseries.tsx |   6 +-
 .../src/MixedTimeseries/index.ts                   |   2 +-
 .../plugins/plugin-chart-echarts/src/Pie/index.ts  |   2 +-
 .../plugin-chart-echarts/src/Radar/index.ts        |   2 +-
 .../src/Timeseries/Area/index.ts                   |   2 +-
 .../src/Timeseries/EchartsTimeseries.tsx           |   6 +-
 .../src/Timeseries/Regular/Bar/index.ts            |   2 +-
 .../src/Timeseries/Regular/Line/index.ts           |   2 +-
 .../src/Timeseries/Regular/Scatter/index.ts        |   2 +-
 .../src/Timeseries/Regular/SmoothLine/index.ts     |   2 +-
 .../src/Timeseries/Step/index.ts                   |   2 +-
 .../plugin-chart-echarts/src/Timeseries/index.ts   |   2 +-
 .../src/Treemap/EchartsTreemap.tsx                 |   9 +-
 .../plugin-chart-echarts/src/Treemap/index.ts      |   2 +-
 .../plugins/plugin-chart-echarts/src/types.ts      |   4 +-
 .../src/utils/eventHandlers.ts                     |   6 +-
 .../src/PivotTableChart.tsx                        |   7 +-
 .../plugin-chart-pivot-table/src/plugin/index.ts   |   2 +-
 .../plugins/plugin-chart-pivot-table/src/types.ts  |   4 +-
 .../plugin-chart-table/src/DataTable/DataTable.tsx |   1 +
 .../plugins/plugin-chart-table/src/TableChart.tsx  |   6 +-
 .../plugins/plugin-chart-table/src/index.ts        |   2 +-
 .../plugins/plugin-chart-table/src/types.ts        |   4 +-
 .../spec/fixtures/mockChartQueries.js              |   6 +-
 superset-frontend/src/components/Chart/Chart.jsx   |   3 +-
 .../src/components/Chart/ChartContextMenu.tsx      | 110 ++++---
 .../src/components/Chart/ChartRenderer.jsx         | 123 ++++----
 .../DrillDetail/DrillDetailMenuItems.test.tsx      | 345 +++++++++++++++++++++
 .../Chart/DrillDetail/DrillDetailMenuItems.tsx     | 236 ++++++++++++++
 .../{ => DrillDetail}/DrillDetailModal.test.tsx    |  97 +++---
 .../Chart/{ => DrillDetail}/DrillDetailModal.tsx   |  93 +++---
 .../Chart/DrillDetail}/DrillDetailPane.test.tsx    |   0
 .../Chart/DrillDetail}/DrillDetailPane.tsx         |  30 +-
 .../DrillDetail/DrillDetailTableControls.test.tsx} |   2 +-
 .../DrillDetail/DrillDetailTableControls.tsx}      |   0
 .../Chart/DrillDetail}/index.ts                    |   2 +-
 .../Chart/DrillDetail}/types.ts                    |   0
 .../Chart/DrillDetail}/utils.ts                    |   0
 .../SliceHeaderControls.test.tsx                   |   4 +
 .../components/SliceHeaderControls/index.tsx       |  23 +-
 superset-frontend/src/dashboard/types.ts           |   4 +-
 .../index.ts => types/ChartSource.ts}              |   5 +-
 56 files changed, 947 insertions(+), 308 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts
index edce9053fd..f89435f1bc 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts
@@ -43,13 +43,29 @@ function openModalFromChartContext(targetMenuItem: string) {
   interceptSamples();
 
   cy.wait(500);
-  cy.get('.ant-dropdown')
-    .not('.ant-dropdown-hidden')
-    .first()
-    .find("[role='menu'] [role='menuitem']")
-    .contains(targetMenuItem)
-    .first()
-    .click();
+  if (targetMenuItem.startsWith('Drill to detail by')) {
+    cy.get('.ant-dropdown')
+      .not('.ant-dropdown-hidden')
+      .first()
+      .find("[role='menu'] [role='menuitem'] [title='Drill to detail by']")
+      .trigger('mouseover');
+    cy.wait(500);
+    cy.get('[data-test="drill-to-detail-by-submenu"]')
+      .not('.ant-dropdown-menu-hidden [data-test="drill-to-detail-by-submenu"]')
+      .find('[role="menuitem"]')
+      .contains(new RegExp(`^${targetMenuItem}$`))
+      .first()
+      .click();
+  } else {
+    cy.get('.ant-dropdown')
+      .not('.ant-dropdown-hidden')
+      .first()
+      .find("[role='menu'] [role='menuitem']")
+      .contains(new RegExp(`^${targetMenuItem}$`))
+      .first()
+      .click();
+  }
+
   cy.wait('@samples');
 }
 
@@ -404,6 +420,18 @@ describe('Drill to detail modal', () => {
         });
       });
     });
+
+    describe('Bar Chart', () => {
+      it('opens the modal for unsupported chart without filters', () => {
+        interceptSamples();
+
+        cy.get("[data-test-viz-type='dist_bar'] svg").then($canvas => {
+          cy.wrap($canvas).scrollIntoView().rightclick(70, 150);
+          openModalFromChartContext('Drill to detail');
+          cy.getBySel('filter-val').should('not.exist');
+        });
+      });
+    });
   });
 
   describe('Tier 2 charts', () => {
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
index e647038593..f9f1a360b6 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
@@ -25,6 +25,12 @@ export type HandlerFunction = (...args: unknown[]) => void;
 export enum Behavior {
   INTERACTIVE_CHART = 'INTERACTIVE_CHART',
   NATIVE_FILTER = 'NATIVE_FILTER',
+
+  /**
+   * Include `DRILL_TO_DETAIL` behavior if plugin handles `contextmenu` event
+   * when dimensions are right-clicked on.
+   */
+  DRILL_TO_DETAIL = 'DRILL_TO_DETAIL',
 }
 
 export enum AppSection {
diff --git a/superset-frontend/packages/superset-ui-core/src/query/index.ts b/superset-frontend/packages/superset-ui-core/src/query/index.ts
index bfc75da205..3ea6dad75f 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/index.ts
@@ -35,6 +35,7 @@ export {
   isXAxisSet,
   hasGenericChartAxes,
 } from './getXAxis';
+export { default as extractQueryFields } from './extractQueryFields';
 
 export * from './types/AnnotationLayer';
 export * from './types/QueryFormData';
diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js
index f8de05e0ef..c845c20da8 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js
@@ -121,7 +121,7 @@ function WorldMap(element, props) {
           formattedVal: val,
         },
       ];
-      onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
+      onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
     } else {
       logging.warn(
         t(
diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.js
index d97adfadf3..6303caec08 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.js
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
 import transformProps from './transformProps';
 import thumbnail from './images/thumbnail.png';
 import example1 from './images/WorldMap1.jpg';
@@ -45,6 +45,7 @@ const metadata = new ChartMetadata({
   ],
   thumbnail,
   useLegacyApi: true,
+  behaviors: [Behavior.DRILL_TO_DETAIL],
 });
 
 export default class WorldMapChartPlugin extends ChartPlugin {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts
index 3f45db74cf..75401411a8 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
 import controlPanel from './controlPanel';
 import transformProps from './transformProps';
 import buildQuery from './buildQuery';
@@ -46,6 +46,7 @@ const metadata = new ChartMetadata({
     t('Description'),
   ],
   thumbnail,
+  behaviors: [Behavior.DRILL_TO_DETAIL],
 });
 
 export default class BigNumberTotalChartPlugin extends ChartPlugin<
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx
index 9a27bc000c..b7516561cd 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx
@@ -26,7 +26,7 @@ import {
   computeMaxFontSize,
   BRAND_COLOR,
   styled,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
 } from '@superset-ui/core';
 import { EChartsCoreOption } from 'echarts';
 import Echart from '../components/Echart';
@@ -65,9 +65,9 @@ type BigNumberVisProps = {
   mainColor: string;
   echartOptions: EChartsCoreOption;
   onContextMenu?: (
-    filters: QueryObjectFilterClause[],
     clientX: number,
     clientY: number,
+    filters?: BinaryQueryObjectFilterClause[],
   ) => void;
   xValueFormatter?: TimeFormatter;
   formData?: BigNumberWithTrendlineFormData;
@@ -171,11 +171,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
     const onContextMenu = (e: MouseEvent<HTMLDivElement>) => {
       if (this.props.onContextMenu) {
         e.preventDefault();
-        this.props.onContextMenu(
-          [],
-          e.nativeEvent.clientX,
-          e.nativeEvent.clientY,
-        );
+        this.props.onContextMenu(e.nativeEvent.clientX, e.nativeEvent.clientY);
       }
     };
 
@@ -249,7 +245,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
           const { data } = eventParams;
           if (data) {
             const pointerEvent = eventParams.event.event;
-            const filters: QueryObjectFilterClause[] = [];
+            const filters: BinaryQueryObjectFilterClause[] = [];
             filters.push({
               col: this.props.formData?.granularitySqla,
               grain: this.props.formData?.timeGrainSqla,
@@ -258,9 +254,9 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
               formattedVal: this.props.xValueFormatter?.(data[0]),
             });
             this.props.onContextMenu(
-              filters,
               pointerEvent.clientX,
               pointerEvent.clientY,
+              filters,
             );
           }
         }
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts
index e774db4824..8cd1d2d288 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
 import controlPanel from './controlPanel';
 import transformProps from './transformProps';
 import buildQuery from './buildQuery';
@@ -45,6 +45,7 @@ const metadata = new ChartMetadata({
     t('Trend'),
   ],
   thumbnail,
+  behaviors: [Behavior.DRILL_TO_DETAIL],
 });
 
 export default class BigNumberWithTrendlineChartPlugin extends ChartPlugin<
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
index c97dffe5ac..3c8620e9d8 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
@@ -44,7 +44,7 @@ export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsBoxPlot'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Distribution'),
         credits: ['https://echarts.apache.org'],
         description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
index a483262419..ba5a7f0173 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
@@ -43,7 +43,7 @@ export default class EchartsFunnelChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsFunnel'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('KPI'),
         credits: ['https://echarts.apache.org'],
         description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
index 16a8b6a6cc..a65a380de2 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
@@ -33,7 +33,7 @@ export default class EchartsGaugeChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsGauge'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('KPI'),
         credits: ['https://echarts.apache.org'],
         description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx
index cefd4f9f6a..0f09fe2386 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import React from 'react';
-import { QueryObjectFilterClause } from '@superset-ui/core';
+import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
 import { EventHandlers } from '../types';
 import Echart from '../components/Echart';
 import { GraphChartTransformedProps } from './types';
@@ -47,7 +47,7 @@ export default function EchartsGraph({
         const sourceValue = data.find(item => item.id === e.data.source)?.name;
         const targetValue = data.find(item => item.id === e.data.target)?.name;
         if (sourceValue && targetValue) {
-          const filters: QueryObjectFilterClause[] = [
+          const filters: BinaryQueryObjectFilterClause[] = [
             {
               col: formData.source,
               op: '==',
@@ -61,7 +61,7 @@ export default function EchartsGraph({
               formattedVal: targetValue,
             },
           ];
-          onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
+          onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
         }
       }
     },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
index b354b61a2f..7e3c26a925 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
 import controlPanel from './controlPanel';
 import transformProps from './transformProps';
 import thumbnail from './images/thumbnail.png';
@@ -46,6 +46,7 @@ export default class EchartsGraphChartPlugin extends ChartPlugin {
           t('Transformable'),
         ],
         thumbnail,
+        behaviors: [Behavior.DRILL_TO_DETAIL],
       }),
       transformProps,
     });
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts
index 63b55d51bd..70a068c977 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts
@@ -19,7 +19,7 @@
 import {
   PlainObject,
   QueryFormData,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
 } from '@superset-ui/core';
 import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
 import { SeriesTooltipOption } from 'echarts/types/src/util/types';
@@ -88,8 +88,8 @@ export type tooltipFormatParams = {
 export type GraphChartTransformedProps = EchartsProps & {
   formData: PlainObject;
   onContextMenu?: (
-    filters: QueryObjectFilterClause[],
     clientX: number,
     clientY: number,
+    filters?: BinaryQueryObjectFilterClause[],
   ) => void;
 };
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
index de11fadaec..8a5421d217 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
@@ -21,7 +21,7 @@ import {
   AxisType,
   DataRecordValue,
   DTTM_ALIAS,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
 } from '@superset-ui/core';
 import { EchartsMixedTimeseriesChartTransformedProps } from './types';
 import Echart from '../components/Echart';
@@ -128,7 +128,7 @@ export default function EchartsMixedTimeseries({
               eventParams.seriesName
             ],
           ];
-          const filters: QueryObjectFilterClause[] = [];
+          const filters: BinaryQueryObjectFilterClause[] = [];
           if (xAxis.type === AxisType.time) {
             filters.push({
               col:
@@ -154,7 +154,7 @@ export default function EchartsMixedTimeseries({
               formattedVal: String(values[i]),
             }),
           );
-          onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
+          onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
         }
       }
     },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
index 2f6a9fc577..05bc71604d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
@@ -53,7 +53,7 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsMixedTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts
index 873a6ac234..9f5d61474a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts
@@ -47,7 +47,7 @@ export default class EchartsPieChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsPie'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Part of a Whole'),
         credits: ['https://echarts.apache.org'],
         description:
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
index f57eccdafa..d810a0a321 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
@@ -44,7 +44,7 @@ export default class EchartsRadarChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsRadar'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Ranking'),
         credits: ['https://echarts.apache.org'],
         description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts
index 8b1407b120..200b25616b 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts
@@ -50,7 +50,7 @@ export default class EchartsAreaChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
index 74a1646e4e..a178e5d05d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
@@ -19,7 +19,7 @@
 import React, { useCallback, useEffect, useRef, useState } from 'react';
 import {
   DTTM_ALIAS,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
   AxisType,
 } from '@superset-ui/core';
 import { ViewRootGroup } from 'echarts/types/src/util/types';
@@ -191,7 +191,7 @@ export default function EchartsTimeseries({
             ...(eventParams.name ? [eventParams.name] : []),
             ...labelMap[eventParams.seriesName],
           ];
-          const filters: QueryObjectFilterClause[] = [];
+          const filters: BinaryQueryObjectFilterClause[] = [];
           if (xAxis.type === AxisType.time) {
             filters.push({
               col:
@@ -216,7 +216,7 @@ export default function EchartsTimeseries({
               formattedVal: String(values[i]),
             }),
           );
-          onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
+          onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
         }
       }
     },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts
index cc63d99dee..6ec20be442 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts
@@ -56,7 +56,7 @@ export default class EchartsTimeseriesBarChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts
index e00f2328fb..3a384293e5 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts
@@ -55,7 +55,7 @@ export default class EchartsTimeseriesLineChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts
index 758b75d0ef..489983cfa6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts
@@ -54,7 +54,7 @@ export default class EchartsTimeseriesScatterChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts
index 53c7cdeea7..ae6dc7ad30 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts
@@ -54,7 +54,7 @@ export default class EchartsTimeseriesSmoothLineChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts
index c565a74d93..3fdeb5aa83 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts
@@ -45,7 +45,7 @@ export default class EchartsTimeseriesStepChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts
index e0532e848b..4065a170d0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts
@@ -44,7 +44,7 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Evolution'),
         credits: ['https://echarts.apache.org'],
         description: hasGenericChartAxes
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx
index 3a9e41b3b2..1ff112cedd 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx
@@ -16,7 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DataRecordValue, QueryObjectFilterClause } from '@superset-ui/core';
+import {
+  DataRecordValue,
+  BinaryQueryObjectFilterClause,
+} from '@superset-ui/core';
 import React, { useCallback } from 'react';
 import Echart from '../components/Echart';
 import { NULL_STRING } from '../constants';
@@ -93,7 +96,7 @@ export default function EchartsTreemap({
         const { treePath } = extractTreePathInfo(eventParams.treePathInfo);
         if (treePath.length > 0) {
           const pointerEvent = eventParams.event.event;
-          const filters: QueryObjectFilterClause[] = [];
+          const filters: BinaryQueryObjectFilterClause[] = [];
           treePath.forEach((path, i) =>
             filters.push({
               col: groupby[i],
@@ -102,7 +105,7 @@ export default function EchartsTreemap({
               formattedVal: path,
             }),
           );
-          onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
+          onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
         }
       }
     },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts
index 575bb41fb9..49be2849ac 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts
@@ -46,7 +46,7 @@ export default class EchartsTreemapChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsTreemap'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART],
+        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
         category: t('Part of a Whole'),
         credits: ['https://echarts.apache.org'],
         description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
index 9fc8997120..8c20543e78 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts
@@ -19,7 +19,7 @@
 import {
   HandlerFunction,
   QueryFormColumn,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
   SetDataMaskHook,
 } from '@superset-ui/core';
 import { EChartsCoreOption, ECharts } from 'echarts';
@@ -116,9 +116,9 @@ export interface EChartTransformedProps<F> {
   selectedValues: Record<number, string>;
   legendData?: OptionName[];
   onContextMenu?: (
-    filters: QueryObjectFilterClause[],
     clientX: number,
     clientY: number,
+    filters?: BinaryQueryObjectFilterClause[],
   ) => void;
 }
 
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts
index fc0b271638..d7c552edfc 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { QueryObjectFilterClause } from '@superset-ui/core';
+import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
 import { EChartTransformedProps, EventHandlers } from '../types';
 
 export type Event = {
@@ -48,7 +48,7 @@ export const contextMenuEventHandler =
     if (onContextMenu) {
       e.event.stop();
       const pointerEvent = e.event.event;
-      const filters: QueryObjectFilterClause[] = [];
+      const filters: BinaryQueryObjectFilterClause[] = [];
       if (groupby.length > 0) {
         const values = labelMap[e.name];
         groupby.forEach((dimension, i) =>
@@ -60,7 +60,7 @@ export const contextMenuEventHandler =
           }),
         );
       }
-      onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
+      onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
     }
   };
 
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
index 4d740148f7..499f072a50 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
@@ -28,7 +28,7 @@ import {
   styled,
   useTheme,
   isAdhocColumn,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
 } from '@superset-ui/core';
 import { PivotTable, sortAs, aggregatorTemplates } from './react-pivottable';
 import {
@@ -370,7 +370,8 @@ export default function PivotTableChart(props: PivotTableProps) {
     ) => {
       if (onContextMenu) {
         e.preventDefault();
-        const filters: QueryObjectFilterClause[] = [];
+        e.stopPropagation();
+        const filters: BinaryQueryObjectFilterClause[] = [];
         if (colKey && colKey.length > 1) {
           colKey.forEach((val, i) => {
             const col = cols[i];
@@ -399,7 +400,7 @@ export default function PivotTableChart(props: PivotTableProps) {
             });
           });
         }
-        onContextMenu(filters, e.clientX, e.clientY);
+        onContextMenu(e.clientX, e.clientY, filters);
       }
     },
     [cols, dateFormatters, onContextMenu, rows],
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/index.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/index.ts
index 423ac59625..f65eefebbe 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/index.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/index.ts
@@ -46,7 +46,7 @@ export default class PivotTableChartPlugin extends ChartPlugin<
    */
   constructor() {
     const metadata = new ChartMetadata({
-      behaviors: [Behavior.INTERACTIVE_CHART],
+      behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
       category: t('Table'),
       description: t(
         'Used to summarize a set of data by grouping together multiple statistics along two axes. Examples: Sales numbers by region and month, tasks by status and assignee, active users by age and location. Not the most visually stunning visualization, but highly informative and versatile.',
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
index ded6c48b2e..accd68a2e4 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/types.ts
@@ -26,7 +26,7 @@ import {
   NumberFormatter,
   QueryFormMetric,
   QueryFormColumn,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
 } from '@superset-ui/core';
 import { ColorFormatters } from '@superset-ui/chart-controls';
 
@@ -74,9 +74,9 @@ interface PivotTableCustomizeProps {
   legacy_order_by: QueryFormMetric[] | QueryFormMetric | null;
   order_desc: boolean;
   onContextMenu?: (
-    filters: QueryObjectFilterClause[],
     clientX: number,
     clientY: number,
+    filters?: BinaryQueryObjectFilterClause[],
   ) => void;
 }
 
diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
index d107f2e5e1..941887afd1 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx
@@ -279,6 +279,7 @@ export default typedMemo(function DataTable<D extends object>({
                 onContextMenu={(e: MouseEvent) => {
                   if (onContextMenu) {
                     e.preventDefault();
+                    e.stopPropagation();
                     onContextMenu(
                       row.original,
                       e.nativeEvent.clientX,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index 83cc9c0cc4..067da59630 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -40,7 +40,7 @@ import {
   ensureIsArray,
   GenericDataType,
   getTimeFormatterForGranularity,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
   styled,
   css,
   t,
@@ -630,7 +630,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
   const handleContextMenu =
     onContextMenu && !isRawRecords
       ? (value: D, clientX: number, clientY: number) => {
-          const filters: QueryObjectFilterClause[] = [];
+          const filters: BinaryQueryObjectFilterClause[] = [];
           columnsMeta.forEach(col => {
             if (!col.isMetric) {
               const dataRecordValue = value[col.key];
@@ -642,7 +642,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
               });
             }
           });
-          onContextMenu(filters, clientX, clientY);
+          onContextMenu(clientX, clientY, filters);
         }
       : undefined;
 
diff --git a/superset-frontend/plugins/plugin-chart-table/src/index.ts b/superset-frontend/plugins/plugin-chart-table/src/index.ts
index bce2112d92..4e862fc5a5 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/index.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/index.ts
@@ -31,7 +31,7 @@ export { default as __hack__ } from './types';
 export * from './types';
 
 const metadata = new ChartMetadata({
-  behaviors: [Behavior.INTERACTIVE_CHART],
+  behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
   category: t('Table'),
   canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
   description: t(
diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts
index 6a5cb88f44..1a6f06f4f8 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts
@@ -30,7 +30,7 @@ import {
   ChartDataResponseResult,
   QueryFormData,
   SetDataMaskHook,
-  QueryObjectFilterClause,
+  BinaryQueryObjectFilterClause,
 } from '@superset-ui/core';
 import { ColorFormatters, ColumnConfig } from '@superset-ui/chart-controls';
 
@@ -113,9 +113,9 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
   columnColorFormatters?: ColorFormatters;
   allowRearrangeColumns?: boolean;
   onContextMenu?: (
-    filters: QueryObjectFilterClause[],
     clientX: number,
     clientY: number,
+    filters?: BinaryQueryObjectFilterClause[],
   ) => void;
 }
 
diff --git a/superset-frontend/spec/fixtures/mockChartQueries.js b/superset-frontend/spec/fixtures/mockChartQueries.js
index 0175df981a..dc29d71abb 100644
--- a/superset-frontend/spec/fixtures/mockChartQueries.js
+++ b/superset-frontend/spec/fixtures/mockChartQueries.js
@@ -37,13 +37,13 @@ export default {
       viz_type: 'pie',
       slice_id: sliceId,
       slice_name: 'Genders',
-      granularity_sqla: null,
-      time_grain_sqla: null,
+      granularity_sqla: undefined,
+      time_grain_sqla: undefined,
       since: '100 years ago',
       until: 'now',
       metrics: ['sum__num'],
       groupby: ['gender'],
-      limit: '25',
+      limit: 25,
       pie_label_type: 'key',
       donut: false,
       show_legend: true,
diff --git a/superset-frontend/src/components/Chart/Chart.jsx b/superset-frontend/src/components/Chart/Chart.jsx
index 38b092bc87..8be3699450 100644
--- a/superset-frontend/src/components/Chart/Chart.jsx
+++ b/superset-frontend/src/components/Chart/Chart.jsx
@@ -29,6 +29,7 @@ import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
 import { URL_PARAMS } from 'src/constants';
 import { getUrlParam } from 'src/utils/urlUtils';
 import { isCurrentUserBot } from 'src/utils/isBot';
+import { ChartSource } from 'src/types/ChartSource';
 import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
 import ChartRenderer from './ChartRenderer';
 import { ChartErrorMessage } from './ChartErrorMessage';
@@ -237,7 +238,7 @@ class Chart extends React.PureComponent {
         subtitle={<MonospaceDiv>{message}</MonospaceDiv>}
         copyText={message}
         link={queryResponse ? queryResponse.link : null}
-        source={dashboardId ? 'dashboard' : 'explore'}
+        source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
         stackTrace={chartStackTrace}
       />
     );
diff --git a/superset-frontend/src/components/Chart/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu.tsx
index 2eba7bbaad..784c6bdd65 100644
--- a/superset-frontend/src/components/Chart/ChartContextMenu.tsx
+++ b/superset-frontend/src/components/Chart/ChartContextMenu.tsx
@@ -23,82 +23,94 @@ import React, {
   useImperativeHandle,
   useState,
 } from 'react';
-import { QueryObjectFilterClause, t, styled } from '@superset-ui/core';
+import ReactDOM from 'react-dom';
+import { useSelector } from 'react-redux';
+import {
+  BinaryQueryObjectFilterClause,
+  FeatureFlag,
+  isFeatureEnabled,
+  QueryFormData,
+} from '@superset-ui/core';
+import { RootState } from 'src/dashboard/types';
+import { findPermission } from 'src/utils/findPermission';
 import { Menu } from 'src/components/Menu';
 import { AntdDropdown as Dropdown } from 'src/components';
-import ReactDOM from 'react-dom';
+import { DrillDetailMenuItems } from './DrillDetail';
 
 const MENU_ITEM_HEIGHT = 32;
 const MENU_VERTICAL_SPACING = 32;
 
 export interface ChartContextMenuProps {
-  id: string;
-  onSelection: (filters: QueryObjectFilterClause[]) => void;
+  id: number;
+  formData: QueryFormData;
+  onSelection: () => void;
   onClose: () => void;
 }
 
 export interface Ref {
   open: (
-    filters: QueryObjectFilterClause[],
     clientX: number,
     clientY: number,
+    filters?: BinaryQueryObjectFilterClause[],
   ) => void;
 }
 
-const Filter = styled.span`
-  ${({ theme }) => `
-    font-weight: ${theme.typography.weights.bold};
-    color: ${theme.colors.primary.base};
-  `}
-`;
-
 const ChartContextMenu = (
-  { id, onSelection, onClose }: ChartContextMenuProps,
+  { id, formData, onSelection, onClose }: ChartContextMenuProps,
   ref: RefObject<Ref>,
 ) => {
-  const [state, setState] = useState<{
-    filters: QueryObjectFilterClause[];
+  const canExplore = useSelector((state: RootState) =>
+    findPermission('can_explore', 'Superset', state.user?.roles),
+  );
+
+  const [{ filters, clientX, clientY }, setState] = useState<{
     clientX: number;
     clientY: number;
-  }>({ filters: [], clientX: 0, clientY: 0 });
+    filters?: BinaryQueryObjectFilterClause[];
+  }>({ clientX: 0, clientY: 0 });
 
-  const menu = (
-    <Menu>
-      {state.filters.map((filter, i) => (
-        <Menu.Item key={i} onClick={() => onSelection([filter])}>
-          {`${t('Drill to detail by')} `}
-          <Filter>{filter.formattedVal}</Filter>
-        </Menu.Item>
-      ))}
-      {state.filters.length === 0 && (
-        <Menu.Item key="none" onClick={() => onSelection([])}>
-          {t('Drill to detail')}
-        </Menu.Item>
-      )}
-      {state.filters.length > 1 && (
-        <Menu.Item key="all" onClick={() => onSelection(state.filters)}>
-          {`${t('Drill to detail by')} `}
-          <Filter>{t('all')}</Filter>
-        </Menu.Item>
-      )}
-    </Menu>
-  );
+  const menuItems = [];
+  const showDrillToDetail =
+    isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) && canExplore;
+
+  if (showDrillToDetail) {
+    menuItems.push(
+      <DrillDetailMenuItems
+        chartId={id}
+        formData={formData}
+        isContextMenu
+        filters={filters}
+        onSelection={onSelection}
+      />,
+    );
+  }
 
   const open = useCallback(
-    (filters: QueryObjectFilterClause[], clientX: number, clientY: number) => {
+    (
+      clientX: number,
+      clientY: number,
+      filters?: BinaryQueryObjectFilterClause[],
+    ) => {
       // Viewport height
       const vh = Math.max(
         document.documentElement.clientHeight || 0,
         window.innerHeight || 0,
       );
 
-      // +1 for automatically added options such as 'All' and 'Drill to detail'
-      const itemsCount = filters.length + 1;
+      const itemsCount =
+        [
+          showDrillToDetail ? 2 : 0, // Drill to detail always has 2 top-level menu items
+        ].reduce((a, b) => a + b, 0) || 1; // "No actions" appears if no actions in menu
+
       const menuHeight = MENU_ITEM_HEIGHT * itemsCount + MENU_VERTICAL_SPACING;
       // Always show the context menu inside the viewport
       const adjustedY = vh - clientY < menuHeight ? vh - menuHeight : clientY;
 
-      setState({ filters, clientX, clientY: adjustedY });
+      setState({
+        clientX,
+        clientY: adjustedY,
+        filters,
+      });
 
       // Since Ant Design's Dropdown does not offer an imperative API
       // and we can't attach event triggers to charts SVG elements, we
@@ -106,7 +118,7 @@ const ChartContextMenu = (
       // from the charts.
       document.getElementById(`hidden-span-${id}`)?.click();
     },
-    [id],
+    [id, showDrillToDetail],
   );
 
   useImperativeHandle(
@@ -119,7 +131,15 @@ const ChartContextMenu = (
 
   return ReactDOM.createPortal(
     <Dropdown
-      overlay={menu}
+      overlay={
+        <Menu>
+          {menuItems.length ? (
+            menuItems
+          ) : (
+            <Menu.Item disabled>No actions</Menu.Item>
+          )}
+        </Menu>
+      }
       trigger={['click']}
       onVisibleChange={value => !value && onClose()}
     >
@@ -128,8 +148,8 @@ const ChartContextMenu = (
         css={{
           visibility: 'hidden',
           position: 'fixed',
-          top: state.clientY,
-          left: state.clientX,
+          top: clientY,
+          left: clientX,
           width: 1,
           height: 1,
         }}
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx
index d1584441e3..e1d3f7290a 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.jsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx
@@ -26,11 +26,12 @@ import {
   t,
   isFeatureEnabled,
   FeatureFlag,
+  getChartMetadataRegistry,
 } from '@superset-ui/core';
 import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
 import { EmptyStateBig, EmptyStateSmall } from 'src/components/EmptyState';
+import { ChartSource } from 'src/types/ChartSource';
 import ChartContextMenu from './ChartContextMenu';
-import DrillDetailModal from './DrillDetailModal';
 
 const propTypes = {
   annotationData: PropTypes.object,
@@ -60,7 +61,7 @@ const propTypes = {
   onFilterMenuClose: PropTypes.func,
   ownState: PropTypes.object,
   postTransformProps: PropTypes.func,
-  source: PropTypes.oneOf(['dashboard', 'explore']),
+  source: PropTypes.oneOf([ChartSource.Dashboard, ChartSource.Explore]),
 };
 
 const BLANK = {};
@@ -83,8 +84,10 @@ class ChartRenderer extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
+      showContextMenu:
+        props.source === ChartSource.Dashboard &&
+        isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL),
       inContextMenu: false,
-      drillDetailFilters: null,
     };
     this.hasQueryResponseChange = false;
 
@@ -97,14 +100,13 @@ class ChartRenderer extends React.Component {
     this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
     this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
     this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
-
-    const showContextMenu =
-      props.source === 'dashboard' &&
-      isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL);
+    this.onContextMenuFallback = this.onContextMenuFallback.bind(this);
 
     this.hooks = {
       onAddFilter: this.handleAddFilter,
-      onContextMenu: showContextMenu ? this.handleOnContextMenu : undefined,
+      onContextMenu: this.state.showContextMenu
+        ? this.handleOnContextMenu
+        : undefined,
       onError: this.handleRenderFailure,
       setControlValue: this.handleSetControlValue,
       onFilterMenuOpen: this.props.onFilterMenuOpen,
@@ -198,19 +200,28 @@ class ChartRenderer extends React.Component {
     }
   }
 
-  handleOnContextMenu(filters, offsetX, offsetY) {
-    this.contextMenuRef.current.open(filters, offsetX, offsetY);
+  handleOnContextMenu(offsetX, offsetY, filters) {
+    this.contextMenuRef.current.open(offsetX, offsetY, filters);
     this.setState({ inContextMenu: true });
   }
 
-  handleContextMenuSelected(filters) {
-    this.setState({ inContextMenu: false, drillDetailFilters: filters });
+  handleContextMenuSelected() {
+    this.setState({ inContextMenu: false });
   }
 
   handleContextMenuClosed() {
     this.setState({ inContextMenu: false });
   }
 
+  // When viz plugins don't handle `contextmenu` event, fallback handler
+  // calls `handleOnContextMenu` with no `filters` param.
+  onContextMenuFallback(event) {
+    if (!this.state.inContextMenu) {
+      event.preventDefault();
+      this.handleOnContextMenu(event.clientX, event.clientY);
+    }
+  }
+
   render() {
     const { chartAlert, chartStatus, chartId } = this.props;
 
@@ -265,7 +276,7 @@ class ChartRenderer extends React.Component {
     let noResultsComponent;
     const noResultTitle = t('No results were returned for this query');
     const noResultDescription =
-      this.props.source === 'explore'
+      this.props.source === ChartSource.Explore
         ? t(
             'Make sure that the controls are configured properly and the datasource contains data for the selected time range',
           )
@@ -285,47 +296,55 @@ class ChartRenderer extends React.Component {
       );
     }
 
+    // Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to
+    // Detail props or if it'll cause side-effects (e.g. excessive re-renders).
+    const drillToDetailProps = getChartMetadataRegistry()
+      .get(formData.viz_type)
+      ?.behaviors.find(behavior => behavior === Behavior.DRILL_TO_DETAIL)
+      ? { inContextMenu: this.state.inContextMenu }
+      : {};
+
     return (
-      <div>
-        {this.props.source === 'dashboard' && (
-          <>
-            <ChartContextMenu
-              ref={this.contextMenuRef}
-              id={chartId}
-              onSelection={this.handleContextMenuSelected}
-              onClose={this.handleContextMenuClosed}
-            />
-            <DrillDetailModal
-              chartId={chartId}
-              initialFilters={this.state.drillDetailFilters}
-              formData={currentFormData}
-            />
-          </>
+      <>
+        {this.state.showContextMenu && (
+          <ChartContextMenu
+            ref={this.contextMenuRef}
+            id={chartId}
+            formData={currentFormData}
+            onSelection={this.handleContextMenuSelected}
+            onClose={this.handleContextMenuClosed}
+          />
         )}
-        <SuperChart
-          disableErrorBoundary
-          key={`${chartId}${webpackHash}`}
-          id={`chart-id-${chartId}`}
-          className={chartClassName}
-          chartType={vizType}
-          width={width}
-          height={height}
-          annotationData={annotationData}
-          datasource={datasource}
-          initialValues={initialValues}
-          formData={currentFormData}
-          ownState={ownState}
-          filterState={filterState}
-          hooks={this.hooks}
-          behaviors={behaviors}
-          queriesData={queriesResponse}
-          onRenderSuccess={this.handleRenderSuccess}
-          onRenderFailure={this.handleRenderFailure}
-          noResults={noResultsComponent}
-          postTransformProps={postTransformProps}
-          inContextMenu={this.state.inContextMenu}
-        />
-      </div>
+        <div
+          onContextMenu={
+            this.state.showContextMenu ? this.onContextMenuFallback : undefined
+          }
+        >
+          <SuperChart
+            disableErrorBoundary
+            key={`${chartId}${webpackHash}`}
+            id={`chart-id-${chartId}`}
+            className={chartClassName}
+            chartType={vizType}
+            width={width}
+            height={height}
+            annotationData={annotationData}
+            datasource={datasource}
+            initialValues={initialValues}
+            formData={currentFormData}
+            ownState={ownState}
+            filterState={filterState}
+            hooks={this.hooks}
+            behaviors={behaviors}
+            queriesData={queriesResponse}
+            onRenderSuccess={this.handleRenderSuccess}
+            onRenderFailure={this.handleRenderFailure}
+            noResults={noResultsComponent}
+            postTransformProps={postTransformProps}
+            {...drillToDetailProps}
+          />
+        </div>
+      </>
     );
   }
 }
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx
new file mode 100644
index 0000000000..8a0f8dbfc5
--- /dev/null
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx
@@ -0,0 +1,345 @@
+/**
+ * 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.
+ */
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import { render, screen, within } from 'spec/helpers/testing-library';
+import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
+import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
+import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
+import { Menu } from 'src/components/Menu';
+import DrillDetailMenuItems, {
+  DrillDetailMenuItemsProps,
+} from './DrillDetailMenuItems';
+
+/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
+
+jest.mock(
+  './DrillDetailPane',
+  () =>
+    ({ initialFilters }: { initialFilters: BinaryQueryObjectFilterClause[] }) =>
+      <pre data-test="modal-filters">{JSON.stringify(initialFilters)}</pre>,
+);
+
+const { id: defaultChartId, form_data: defaultFormData } =
+  chartQueries[sliceId];
+
+const { slice_name: chartName } = defaultFormData;
+const unsupportedChartFormData = {
+  ...defaultFormData,
+  viz_type: 'dist_bar',
+};
+
+const noDimensionsFormData = {
+  ...defaultFormData,
+  viz_type: 'table',
+  query_mode: 'raw',
+};
+
+const filterA: BinaryQueryObjectFilterClause = {
+  col: 'sample_column',
+  op: '==',
+  val: 1234567890,
+  formattedVal: 'Yesterday',
+};
+
+const filterB: BinaryQueryObjectFilterClause = {
+  col: 'sample_column_2',
+  op: '==',
+  val: 987654321,
+  formattedVal: 'Two days ago',
+};
+
+const renderMenu = ({
+  chartId,
+  formData,
+  isContextMenu,
+  filters,
+}: Partial<DrillDetailMenuItemsProps>) => {
+  const store = getMockStoreWithNativeFilters();
+  return render(
+    <Menu>
+      <DrillDetailMenuItems
+        chartId={chartId ?? defaultChartId}
+        formData={formData ?? defaultFormData}
+        filters={filters}
+        isContextMenu={isContextMenu}
+      />
+    </Menu>,
+    { useRouter: true, useRedux: true, store },
+  );
+};
+
+/**
+ * Drill to Detail modal should appear with correct initial filters
+ */
+const expectDrillToDetailModal = async (
+  buttonName: string,
+  filters: BinaryQueryObjectFilterClause[] = [],
+) => {
+  const button = screen.getByRole('menuitem', { name: buttonName });
+  userEvent.click(button);
+  const modal = await screen.findByRole('dialog', {
+    name: `Drill to detail: ${chartName}`,
+  });
+
+  expect(modal).toBeVisible();
+  expect(screen.getByTestId('modal-filters')).toHaveTextContent(
+    JSON.stringify(filters),
+  );
+};
+
+/**
+ * Menu item should be enabled without explanatory tooltip
+ */
+const expectMenuItemEnabled = async (menuItem: HTMLElement) => {
+  expect(menuItem).toBeInTheDocument();
+  expect(menuItem).not.toHaveAttribute('aria-disabled');
+  const tooltipTrigger = within(menuItem).queryByTestId('tooltip-trigger');
+  expect(tooltipTrigger).not.toBeInTheDocument();
+};
+
+/**
+ * Menu item should be disabled, optionally with an explanatory tooltip
+ */
+const expectMenuItemDisabled = async (
+  menuItem: HTMLElement,
+  tooltipContent?: string,
+) => {
+  expect(menuItem).toBeVisible();
+  expect(menuItem).toHaveAttribute('aria-disabled', 'true');
+  const tooltipTrigger = within(menuItem).queryByTestId('tooltip-trigger');
+  if (tooltipContent) {
+    userEvent.hover(tooltipTrigger as HTMLElement);
+    const tooltip = await screen.findByRole('tooltip', {
+      name: tooltipContent,
+    });
+
+    expect(tooltip).toBeInTheDocument();
+  } else {
+    expect(tooltipTrigger).not.toBeInTheDocument();
+  }
+};
+
+/**
+ * "Drill to detail" item should be enabled and open the correct modal
+ */
+const expectDrillToDetailEnabled = async () => {
+  const drillToDetailMenuItem = screen.getByRole('menuitem', {
+    name: 'Drill to detail',
+  });
+
+  await expectMenuItemEnabled(drillToDetailMenuItem);
+  await expectDrillToDetailModal('Drill to detail');
+};
+
+/**
+ * "Drill to detail" item should be present and disabled
+ */
+const expectDrillToDetailDisabled = async (tooltipContent?: string) => {
+  const drillToDetailMenuItem = screen.getByRole('menuitem', {
+    name: 'Drill to detail',
+  });
+
+  await expectMenuItemDisabled(drillToDetailMenuItem, tooltipContent);
+};
+
+/**
+ * "Drill to detail by" item should not be present
+ */
+const expectNoDrillToDetailBy = async () => {
+  const drillToDetailBy = screen.queryByRole('menuitem', {
+    name: 'Drill to detail by',
+  });
+
+  expect(drillToDetailBy).not.toBeInTheDocument();
+};
+
+/**
+ * "Drill to detail by" submenu should be present and enabled
+ */
+const expectDrillToDetailByEnabled = async () => {
+  const drillToDetailBy = screen.getByRole('menuitem', {
+    name: 'Drill to detail by',
+  });
+
+  await expectMenuItemEnabled(drillToDetailBy);
+  userEvent.hover(
+    within(drillToDetailBy).getByRole('button', { name: 'Drill to detail by' }),
+  );
+
+  expect(
+    await screen.findByTestId('drill-to-detail-by-submenu'),
+  ).toBeInTheDocument();
+};
+
+/**
+ * "Drill to detail by" submenu should be present and disabled
+ */
+const expectDrillToDetailByDisabled = async (tooltipContent?: string) => {
+  const drillToDetailBySubmenuItem = screen.getByRole('menuitem', {
+    name: 'Drill to detail by',
+  });
+
+  await expectMenuItemDisabled(drillToDetailBySubmenuItem, tooltipContent);
+};
+
+/**
+ * "Drill to detail by {dimension}" submenu item should exist and open the correct modal
+ */
+const expectDrillToDetailByDimension = async (
+  filter: BinaryQueryObjectFilterClause,
+) => {
+  userEvent.hover(screen.getByRole('button', { name: 'Drill to detail by' }));
+  const drillToDetailBySubMenu = await screen.findByTestId(
+    'drill-to-detail-by-submenu',
+  );
+
+  const menuItemName = `Drill to detail by ${filter.formattedVal}`;
+  const drillToDetailBySubmenuItem = within(drillToDetailBySubMenu).getByRole(
+    'menuitem',
+    { name: menuItemName },
+  );
+
+  await expectMenuItemEnabled(drillToDetailBySubmenuItem);
+  await expectDrillToDetailModal(menuItemName, [filter]);
+};
+
+/**
+ * "Drill to detail by all" submenu item should exist and open the correct modal
+ */
+const expectDrillToDetailByAll = async (
+  filters: BinaryQueryObjectFilterClause[],
+) => {
+  userEvent.hover(screen.getByRole('button', { name: 'Drill to detail by' }));
+  const drillToDetailBySubMenu = await screen.findByTestId(
+    'drill-to-detail-by-submenu',
+  );
+
+  const menuItemName = 'Drill to detail by all';
+  const drillToDetailBySubmenuItem = within(drillToDetailBySubMenu).getByRole(
+    'menuitem',
+    { name: menuItemName },
+  );
+
+  await expectMenuItemEnabled(drillToDetailBySubmenuItem);
+  await expectDrillToDetailModal(menuItemName, filters);
+};
+
+test('dropdown menu for unsupported chart', async () => {
+  renderMenu({ formData: unsupportedChartFormData });
+  await expectDrillToDetailEnabled();
+  await expectNoDrillToDetailBy();
+});
+
+test('context menu for unsupported chart', async () => {
+  renderMenu({
+    formData: unsupportedChartFormData,
+    isContextMenu: true,
+  });
+
+  await expectDrillToDetailEnabled();
+  await expectDrillToDetailByDisabled(
+    'Drill to detail by value is not yet supported for this chart type.',
+  );
+});
+
+test('dropdown menu for supported chart, no dimensions', async () => {
+  renderMenu({
+    formData: noDimensionsFormData,
+  });
+
+  await expectDrillToDetailDisabled(
+    'Drill to detail is disabled because this chart does not group data by dimension value.',
+  );
+
+  await expectNoDrillToDetailBy();
+});
+
+test('context menu for supported chart, no dimensions, no filters', async () => {
+  renderMenu({
+    formData: noDimensionsFormData,
+    isContextMenu: true,
+  });
+
+  await expectDrillToDetailDisabled(
+    'Drill to detail is disabled because this chart does not group data by dimension value.',
+  );
+
+  await expectDrillToDetailByDisabled();
+});
+
+test('context menu for supported chart, no dimensions, 1 filter', async () => {
+  renderMenu({
+    formData: noDimensionsFormData,
+    isContextMenu: true,
+    filters: [filterA],
+  });
+
+  await expectDrillToDetailDisabled(
+    'Drill to detail is disabled because this chart does not group data by dimension value.',
+  );
+
+  await expectDrillToDetailByDisabled();
+});
+
+test('dropdown menu for supported chart, dimensions', async () => {
+  renderMenu({ formData: defaultFormData });
+  await expectDrillToDetailEnabled();
+  await expectNoDrillToDetailBy();
+});
+
+test('context menu for supported chart, dimensions, no filters', async () => {
+  renderMenu({
+    formData: defaultFormData,
+    isContextMenu: true,
+  });
+
+  await expectDrillToDetailEnabled();
+  await expectDrillToDetailByDisabled(
+    'Right-click on a dimension value to drill to detail by that value.',
+  );
+});
+
+test('context menu for supported chart, dimensions, 1 filter', async () => {
+  const filters = [filterA];
+  renderMenu({
+    formData: defaultFormData,
+    isContextMenu: true,
+    filters,
+  });
+
+  await expectDrillToDetailEnabled();
+  await expectDrillToDetailByEnabled();
+  await expectDrillToDetailByDimension(filterA);
+});
+
+test('context menu for supported chart, dimensions, 2 filters', async () => {
+  const filters = [filterA, filterB];
+  renderMenu({
+    formData: defaultFormData,
+    isContextMenu: true,
+    filters,
+  });
+
+  await expectDrillToDetailEnabled();
+  await expectDrillToDetailByEnabled();
+  await expectDrillToDetailByDimension(filterA);
+  await expectDrillToDetailByDimension(filterB);
+  await expectDrillToDetailByAll(filters);
+});
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
new file mode 100644
index 0000000000..8269e85900
--- /dev/null
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
@@ -0,0 +1,236 @@
+/**
+ * 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.
+ */
+
+import React, { ReactNode, useCallback, useMemo, useState } from 'react';
+import { isEmpty } from 'lodash';
+import {
+  Behavior,
+  BinaryQueryObjectFilterClause,
+  css,
+  extractQueryFields,
+  getChartMetadataRegistry,
+  QueryFormData,
+  styled,
+  SupersetTheme,
+  t,
+} from '@superset-ui/core';
+import { Menu } from 'src/components/Menu';
+import Icons from 'src/components/Icons';
+import { Tooltip } from 'src/components/Tooltip';
+import DrillDetailModal from './DrillDetailModal';
+
+const DisabledMenuItemTooltip = ({ title }: { title: ReactNode }) => (
+  <Tooltip title={title} placement="top">
+    <Icons.InfoCircleOutlined
+      data-test="tooltip-trigger"
+      css={(theme: SupersetTheme) => css`
+        color: ${theme.colors.text.label};
+        margin-left: ${theme.gridUnit * 2}px;
+        &.anticon {
+          font-size: unset;
+          .anticon {
+            line-height: unset;
+            vertical-align: unset;
+          }
+        }
+      `}
+    />
+  </Tooltip>
+);
+
+const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
+  <Menu.Item disabled {...props}>
+    <div
+      css={css`
+        white-space: normal;
+        max-width: 160px;
+      `}
+    >
+      {children}
+    </div>
+  </Menu.Item>
+);
+
+const Filter = styled.span`
+  ${({ theme }) => `
+     font-weight: ${theme.typography.weights.bold};
+     color: ${theme.colors.primary.base};
+   `}
+`;
+
+export type DrillDetailMenuItemsProps = {
+  chartId: number;
+  formData: QueryFormData;
+  filters?: BinaryQueryObjectFilterClause[];
+  isContextMenu?: boolean;
+  onSelection?: () => void;
+  onClick?: (event: MouseEvent) => void;
+};
+
+const DrillDetailMenuItems = ({
+  chartId,
+  formData,
+  filters = [],
+  isContextMenu = false,
+  onSelection = () => null,
+  onClick = () => null,
+  ...props
+}: DrillDetailMenuItemsProps) => {
+  const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
+    [],
+  );
+
+  const [showModal, setShowModal] = useState(false);
+  const openModal = useCallback(
+    (filters, event) => {
+      onClick(event);
+      onSelection();
+      setFilters(filters);
+      setShowModal(true);
+    },
+    [onClick, onSelection],
+  );
+
+  const closeModal = useCallback(() => {
+    setShowModal(false);
+  }, []);
+
+  // Check for Behavior.DRILL_TO_DETAIL to tell if plugin handles the `contextmenu`
+  // event for dimensions.  If it doesn't, tell the user that drill to detail by
+  // dimension is not supported.  If it does, and the `contextmenu` handler didn't
+  // pass any filters, tell the user that they didn't select a dimension.
+  const handlesDimensionContextMenu = useMemo(
+    () =>
+      getChartMetadataRegistry()
+        .get(formData.viz_type)
+        ?.behaviors.find(behavior => behavior === Behavior.DRILL_TO_DETAIL),
+    [formData.viz_type],
+  );
+
+  // Check metrics to see if chart's current configuration lacks
+  // aggregations, in which case Drill to Detail should be disabled.
+  const noAggregations = useMemo(() => {
+    const { metrics } = extractQueryFields(formData);
+    return isEmpty(metrics);
+  }, [formData]);
+
+  let drillToDetailMenuItem;
+  if (handlesDimensionContextMenu && noAggregations) {
+    drillToDetailMenuItem = (
+      <DisabledMenuItem {...props} key="drill-detail-no-aggregations">
+        {t('Drill to detail')}
+        <DisabledMenuItemTooltip
+          title={t(
+            'Drill to detail is disabled because this chart does not group data by dimension value.',
+          )}
+        />
+      </DisabledMenuItem>
+    );
+  } else {
+    drillToDetailMenuItem = (
+      <Menu.Item
+        {...props}
+        key="drill-detail-no-filters"
+        onClick={openModal.bind(null, [])}
+      >
+        {t('Drill to detail')}
+      </Menu.Item>
+    );
+  }
+
+  let drillToDetailByMenuItem;
+  if (!handlesDimensionContextMenu) {
+    drillToDetailByMenuItem = (
+      <DisabledMenuItem {...props} key="drill-detail-by-chart-not-supported">
+        {t('Drill to detail by')}
+        <DisabledMenuItemTooltip
+          title={t(
+            'Drill to detail by value is not yet supported for this chart type.',
+          )}
+        />
+      </DisabledMenuItem>
+    );
+  }
+
+  if (handlesDimensionContextMenu && noAggregations) {
+    drillToDetailByMenuItem = (
+      <DisabledMenuItem {...props} key="drill-detail-by-no-aggregations">
+        {t('Drill to detail by')}
+      </DisabledMenuItem>
+    );
+  }
+
+  if (handlesDimensionContextMenu && !noAggregations && filters?.length) {
+    drillToDetailByMenuItem = (
+      <Menu.SubMenu {...props} title={t('Drill to detail by')}>
+        <div data-test="drill-to-detail-by-submenu">
+          {filters.map((filter, i) => (
+            <Menu.Item
+              {...props}
+              key={`drill-detail-filter-${i}`}
+              onClick={openModal.bind(null, [filter])}
+            >
+              {`${t('Drill to detail by')} `}
+              <Filter>{filter.formattedVal}</Filter>
+            </Menu.Item>
+          ))}
+          {filters.length > 1 && (
+            <Menu.Item
+              {...props}
+              key="drill-detail-filter-all"
+              onClick={openModal.bind(null, filters)}
+            >
+              {`${t('Drill to detail by')} `}
+              <Filter>{t('all')}</Filter>
+            </Menu.Item>
+          )}
+        </div>
+      </Menu.SubMenu>
+    );
+  }
+
+  if (handlesDimensionContextMenu && !noAggregations && !filters?.length) {
+    drillToDetailByMenuItem = (
+      <DisabledMenuItem {...props} key="drill-detail-by-select-aggregation">
+        {t('Drill to detail by')}
+        <DisabledMenuItemTooltip
+          title={t(
+            'Right-click on a dimension value to drill to detail by that value.',
+          )}
+        />
+      </DisabledMenuItem>
+    );
+  }
+
+  return (
+    <>
+      {drillToDetailMenuItem}
+      {isContextMenu && drillToDetailByMenuItem}
+      <DrillDetailModal
+        chartId={chartId}
+        formData={formData}
+        initialFilters={modalFilters}
+        showModal={showModal}
+        onHideModal={closeModal}
+      />
+    </>
+  );
+};
+
+export default DrillDetailMenuItems;
diff --git a/superset-frontend/src/components/Chart/DrillDetailModal.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx
similarity index 59%
rename from superset-frontend/src/components/Chart/DrillDetailModal.test.tsx
rename to superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx
index 20f319ce4f..038541d390 100644
--- a/superset-frontend/src/components/Chart/DrillDetailModal.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx
@@ -16,44 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+
+import React, { useState } from 'react';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from 'spec/helpers/testing-library';
 import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
 import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
-import { QueryFormData } from '@superset-ui/core';
-import fetchMock from 'fetch-mock';
-import userEvent from '@testing-library/user-event';
 import DrillDetailModal from './DrillDetailModal';
 
-const chart = chartQueries[sliceId];
-const setup = (overrides: Record<string, any> = {}) => {
-  const store = getMockStoreWithNativeFilters();
-  const props = {
-    chartId: sliceId,
-    initialFilters: [],
-    formData: chart.form_data as unknown as QueryFormData,
-    ...overrides,
-  };
-  return render(<DrillDetailModal {...props} />, {
-    useRedux: true,
-    useRouter: true,
-    store,
-  });
-};
-const waitForRender = (overrides: Record<string, any> = {}) =>
-  waitFor(() => setup(overrides));
-
-fetchMock.post(
-  'end:/datasource/samples?force=false&datasource_type=table&datasource_id=7&per_page=50&page=1',
-  {
-    result: {
-      data: [],
-      colnames: [],
-      coltypes: [],
-    },
-  },
-);
-
+jest.mock('./DrillDetailPane', () => () => null);
 const mockHistoryPush = jest.fn();
 jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
@@ -62,32 +33,46 @@ jest.mock('react-router-dom', () => ({
   }),
 }));
 
-test('should render', async () => {
-  const { container } = await waitForRender();
-  expect(container).toBeInTheDocument();
-});
-
-test('should render the title', async () => {
-  await waitForRender();
-  expect(
-    screen.getByText(`Drill to detail: ${chart.form_data.slice_name}`),
-  ).toBeInTheDocument();
-});
+const { id: chartId, form_data: formData } = chartQueries[sliceId];
+const { slice_name: chartName } = formData;
 
-test('should render the modal', async () => {
-  await waitForRender();
-  expect(screen.getByRole('dialog')).toBeInTheDocument();
-});
+const renderModal = async () => {
+  const store = getMockStoreWithNativeFilters();
+  const DrillDetailModalWrapper = () => {
+    const [showModal, setShowModal] = useState(false);
+    return (
+      <>
+        <button type="button" onClick={() => setShowModal(true)}>
+          Show modal
+        </button>
+        <DrillDetailModal
+          chartId={chartId}
+          formData={formData}
+          initialFilters={[]}
+          showModal={showModal}
+          onHideModal={() => setShowModal(false)}
+        />
+      </>
+    );
+  };
 
-test('should not render the modal', async () => {
-  await waitForRender({
-    initialFilters: undefined,
+  render(<DrillDetailModalWrapper />, {
+    useRouter: true,
+    useRedux: true,
+    store,
   });
-  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+
+  userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
+  await screen.findByRole('dialog', { name: `Drill to detail: ${chartName}` });
+};
+
+test('should render the title', async () => {
+  await renderModal();
+  expect(screen.getByText(`Drill to detail: ${chartName}`)).toBeInTheDocument();
 });
 
 test('should render the button', async () => {
-  await waitForRender();
+  await renderModal();
   expect(
     screen.getByRole('button', { name: 'Edit chart' }),
   ).toBeInTheDocument();
@@ -95,14 +80,14 @@ test('should render the button', async () => {
 });
 
 test('should close the modal', async () => {
-  await waitForRender();
+  await renderModal();
   expect(screen.getByRole('dialog')).toBeInTheDocument();
   userEvent.click(screen.getAllByRole('button', { name: 'Close' })[1]);
   expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
 });
 
 test('should forward to Explore', async () => {
-  await waitForRender();
+  await renderModal();
   userEvent.click(screen.getByRole('button', { name: 'Edit chart' }));
   expect(mockHistoryPush).toHaveBeenCalledWith(
     `/explore/?dashboard_page_id=&slice_id=${sliceId}`,
diff --git a/superset-frontend/src/components/Chart/DrillDetailModal.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx
similarity index 65%
rename from superset-frontend/src/components/Chart/DrillDetailModal.tsx
rename to superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx
index 448cc2566f..160796c308 100644
--- a/superset-frontend/src/components/Chart/DrillDetailModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx
@@ -17,14 +17,7 @@
  * under the License.
  */
 
-import React, {
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useState,
-} from 'react';
-import { useSelector } from 'react-redux';
+import React, { useCallback, useContext, useMemo } from 'react';
 import { useHistory } from 'react-router-dom';
 import {
   BinaryQueryObjectFilterClause,
@@ -33,22 +26,51 @@ import {
   t,
   useTheme,
 } from '@superset-ui/core';
-import DrillDetailPane from 'src/dashboard/components/DrillDetailPane';
+import Modal from 'src/components/Modal';
+import Button from 'src/components/Button';
+import { useSelector } from 'react-redux';
 import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
 import { Slice } from 'src/types/Chart';
-import Modal from '../Modal';
-import Button from '../Button';
+import DrillDetailPane from './DrillDetailPane';
+
+interface ModalFooterProps {
+  exploreChart: () => void;
+  closeModal?: () => void;
+}
 
-const DrillDetailModal: React.FC<{
+const ModalFooter = ({ exploreChart, closeModal }: ModalFooterProps) => (
+  <>
+    <Button buttonStyle="secondary" buttonSize="small" onClick={exploreChart}>
+      {t('Edit chart')}
+    </Button>
+    <Button
+      buttonStyle="primary"
+      buttonSize="small"
+      onClick={closeModal}
+      data-test="close-drilltodetail-modal"
+    >
+      {t('Close')}
+    </Button>
+  </>
+);
+
+interface DrillDetailModalProps {
   chartId: number;
-  initialFilters?: BinaryQueryObjectFilterClause[];
   formData: QueryFormData;
-}> = ({ chartId, initialFilters, formData }) => {
-  const [showModal, setShowModal] = useState(false);
-  const openModal = useCallback(() => setShowModal(true), []);
-  const closeModal = useCallback(() => setShowModal(false), []);
-  const history = useHistory();
+  initialFilters: BinaryQueryObjectFilterClause[];
+  showModal: boolean;
+  onHideModal: () => void;
+}
+
+export default function DrillDetailModal({
+  chartId,
+  formData,
+  initialFilters,
+  showModal,
+  onHideModal,
+}: DrillDetailModalProps) {
   const theme = useTheme();
+  const history = useHistory();
   const dashboardPageId = useContext(DashboardPageIdContext);
   const { slice_name: chartName } = useSelector(
     (state: { sliceEntities: { slices: Record<number, Slice> } }) =>
@@ -64,43 +86,18 @@ const DrillDetailModal: React.FC<{
     history.push(exploreUrl);
   }, [exploreUrl, history]);
 
-  //  Trigger modal open when initial filters change
-  useEffect(() => {
-    if (initialFilters) {
-      openModal();
-    }
-  }, [initialFilters, openModal]);
-
   return (
     <Modal
+      show={showModal}
+      onHide={onHideModal ?? (() => null)}
       css={css`
         .ant-modal-body {
           display: flex;
           flex-direction: column;
         }
       `}
-      show={showModal}
-      onHide={closeModal}
       title={t('Drill to detail: %s', chartName)}
-      footer={
-        <>
-          <Button
-            buttonStyle="secondary"
-            buttonSize="small"
-            onClick={exploreChart}
-          >
-            {t('Edit chart')}
-          </Button>
-          <Button
-            data-test="close-drilltodetail-modal"
-            buttonStyle="primary"
-            buttonSize="small"
-            onClick={closeModal}
-          >
-            {t('Close')}
-          </Button>
-        </>
-      }
+      footer={<ModalFooter exploreChart={exploreChart} />}
       responsive
       resizable
       resizableConfig={{
@@ -117,6 +114,4 @@ const DrillDetailModal: React.FC<{
       <DrillDetailPane formData={formData} initialFilters={initialFilters} />
     </Modal>
   );
-};
-
-export default DrillDetailModal;
+}
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx
similarity index 100%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.test.tsx
rename to superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx
similarity index 91%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
rename to superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx
index ea3c6f5734..7d2d572d12 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx
@@ -44,7 +44,7 @@ import MetadataBar, {
 } from 'src/components/MetadataBar';
 import Alert from 'src/components/Alert';
 import { useApiV1Resource } from 'src/hooks/apiResources';
-import TableControls from './TableControls';
+import TableControls from './DrillDetailTableControls';
 import { getDrillPayload } from './utils';
 import { Dataset, ResultsPage } from './types';
 
@@ -55,12 +55,12 @@ export default function DrillDetailPane({
   initialFilters,
 }: {
   formData: QueryFormData;
-  initialFilters?: BinaryQueryObjectFilterClause[];
+  initialFilters: BinaryQueryObjectFilterClause[];
 }) {
   const theme = useTheme();
   const [pageIndex, setPageIndex] = useState(0);
   const lastPageIndex = useRef(pageIndex);
-  const [filters, setFilters] = useState(initialFilters || []);
+  const [filters, setFilters] = useState(initialFilters);
   const [isLoading, setIsLoading] = useState(false);
   const [responseError, setResponseError] = useState('');
   const [resultsPages, setResultsPages] = useState<Map<number, ResultsPage>>(
@@ -72,13 +72,13 @@ export default function DrillDetailPane({
       state.common.conf.SAMPLES_ROW_LIMIT,
   );
 
-  //  Extract datasource ID/type from string ID
+  // Extract datasource ID/type from string ID
   const [datasourceId, datasourceType] = useMemo(
     () => formData.datasource.split('__'),
     [formData.datasource],
   );
 
-  //  Get page of results
+  // Get page of results
   const resultsPage = useMemo(() => {
     const nextResultsPage = resultsPages.get(pageIndex);
     if (nextResultsPage) {
@@ -98,7 +98,7 @@ export default function DrillDetailPane({
     formData.datasource,
   );
 
-  //  Disable sorting on columns
+  // Disable sorting on columns
   const sortDisabledColumns = useMemo(
     () =>
       columns.map(column => ({
@@ -108,26 +108,26 @@ export default function DrillDetailPane({
     [columns],
   );
 
-  //  Update page index on pagination click
+  // Update page index on pagination click
   const onServerPagination = useCallback(({ pageIndex }) => {
     setPageIndex(pageIndex);
   }, []);
 
-  //  Clear cache on reload button click
+  // Clear cache on reload button click
   const handleReload = useCallback(() => {
     setResponseError('');
     setResultsPages(new Map());
     setPageIndex(0);
   }, []);
 
-  //  Clear cache and reset page index if filters change
+  // Clear cache and reset page index if filters change
   useEffect(() => {
     setResponseError('');
     setResultsPages(new Map());
     setPageIndex(0);
   }, [filters]);
 
-  //  Update cache order if page in cache
+  // Update cache order if page in cache
   useEffect(() => {
     if (
       resultsPages.has(pageIndex) &&
@@ -144,7 +144,7 @@ export default function DrillDetailPane({
     }
   }, [pageIndex, resultsPages]);
 
-  //  Download page of results & trim cache if page not in cache
+  // Download page of results & trim cache if page not in cache
   useEffect(() => {
     if (!responseError && !isLoading && !resultsPages.has(pageIndex)) {
       setIsLoading(true);
@@ -196,7 +196,7 @@ export default function DrillDetailPane({
 
   let tableContent = null;
   if (responseError) {
-    //  Render error if page download failed
+    // Render error if page download failed
     tableContent = (
       <pre
         css={css`
@@ -207,14 +207,14 @@ export default function DrillDetailPane({
       </pre>
     );
   } else if (!resultsPages.size) {
-    //  Render loading if first page hasn't loaded
+    // Render loading if first page hasn't loaded
     tableContent = <Loading />;
   } else if (resultsPage?.total === 0) {
-    //  Render empty state if no results are returned for page
+    // Render empty state if no results are returned for page
     const title = t('No rows were returned for this dataset');
     tableContent = <EmptyStateMedium image="document.svg" title={title} />;
   } else {
-    //  Render table if at least one page has successfully loaded
+    // Render table if at least one page has successfully loaded
     tableContent = (
       <TableView
         columns={sortDisabledColumns}
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.test.tsx
similarity index 98%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.test.tsx
rename to superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.test.tsx
index 0768d0ec73..179fc8ee35 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.test.tsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import { render, screen } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
-import TableControls from './TableControls';
+import TableControls from './DrillDetailTableControls';
 
 const setFilters = jest.fn();
 const onReload = jest.fn();
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx
similarity index 100%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/TableControls.tsx
rename to superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts b/superset-frontend/src/components/Chart/DrillDetail/index.ts
similarity index 91%
copy from superset-frontend/src/dashboard/components/DrillDetailPane/index.ts
copy to superset-frontend/src/components/Chart/DrillDetail/index.ts
index 7e23e0a55c..cf154680be 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts
+++ b/superset-frontend/src/components/Chart/DrillDetail/index.ts
@@ -17,4 +17,4 @@
  * under the License.
  */
 
-export { default } from './DrillDetailPane';
+export { default as DrillDetailMenuItems } from './DrillDetailMenuItems';
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/types.ts b/superset-frontend/src/components/Chart/DrillDetail/types.ts
similarity index 100%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/types.ts
rename to superset-frontend/src/components/Chart/DrillDetail/types.ts
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts b/superset-frontend/src/components/Chart/DrillDetail/utils.ts
similarity index 100%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/utils.ts
rename to superset-frontend/src/components/Chart/DrillDetail/utils.ts
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
index 455afb6039..28d740fc13 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx
@@ -19,6 +19,7 @@
 
 import userEvent from '@testing-library/user-event';
 import React from 'react';
+import { getMockStore } from 'spec/fixtures/mockStore';
 import { render, screen } from 'spec/helpers/testing-library';
 import { FeatureFlag } from 'src/featureFlags';
 import SliceHeaderControls, { SliceHeaderControlsProps } from '.';
@@ -96,9 +97,11 @@ const createProps = (viz_type = 'sunburst') =>
 
 const renderWrapper = (overrideProps?: SliceHeaderControlsProps) => {
   const props = overrideProps || createProps();
+  const store = getMockStore();
   return render(<SliceHeaderControls {...props} />, {
     useRedux: true,
     useRouter: true,
+    store,
   });
 };
 
@@ -253,6 +256,7 @@ test('Should show the "Drill to detail"', () => {
     [FeatureFlag.DRILL_TO_DETAIL]: true,
   };
   const props = createProps();
+  props.slice.slice_id = 18;
   renderWrapper(props);
   expect(screen.getByText('Drill to detail')).toBeInTheDocument();
 });
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
index 19bb612b04..5bdc442ba1 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx
@@ -53,7 +53,7 @@ import Button from 'src/components/Button';
 import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal';
 import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane';
 import Modal from 'src/components/Modal';
-import DrillDetailPane from 'src/dashboard/components/DrillDetailPane';
+import { DrillDetailMenuItems } from 'src/components/Chart/DrillDetail';
 
 const MENU_KEYS = {
   CROSS_FILTER_SCOPING: 'cross_filter_scoping',
@@ -156,7 +156,7 @@ const dropdownIconsStyles = css`
   }
 `;
 
-const DashboardChartModalTrigger = ({
+const ViewResultsModalTrigger = ({
   exploreUrl,
   triggerNode,
   modalTitle,
@@ -205,7 +205,6 @@ const DashboardChartModalTrigger = ({
                 {t('Edit chart')}
               </Button>
               <Button
-                data-test="close-drilltodetail-modal"
                 buttonStyle="primary"
                 buttonSize="small"
                 onClick={closeModal}
@@ -430,7 +429,7 @@ class SliceHeaderControls extends React.PureComponent<
 
         {this.props.supersetCanExplore && (
           <Menu.Item key={MENU_KEYS.VIEW_RESULTS}>
-            <DashboardChartModalTrigger
+            <ViewResultsModalTrigger
               exploreUrl={this.props.exploreUrl}
               triggerNode={
                 <span data-test="view-query-menu-item">
@@ -453,18 +452,10 @@ class SliceHeaderControls extends React.PureComponent<
 
         {isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) &&
           this.props.supersetCanExplore && (
-            <Menu.Item key={MENU_KEYS.DRILL_TO_DETAIL}>
-              <DashboardChartModalTrigger
-                exploreUrl={this.props.exploreUrl}
-                triggerNode={
-                  <span data-test="view-query-menu-item">
-                    {t('Drill to detail')}
-                  </span>
-                }
-                modalTitle={t('Drill to detail: %s', slice.slice_name)}
-                modalBody={<DrillDetailPane formData={this.props.formData} />}
-              />
-            </Menu.Item>
+            <DrillDetailMenuItems
+              chartId={slice.slice_id}
+              formData={this.props.formData}
+            />
           )}
 
         {(slice.description || this.props.supersetCanExplore) && (
diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts
index 31cc4bbded..2c2fbb2812 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -29,7 +29,7 @@ import { chart } from 'src/components/Chart/chartReducer';
 import componentTypes from 'src/dashboard/util/componentTypes';
 import { UrlParamEntries } from 'src/utils/urlUtils';
 
-import { User } from 'src/types/bootstrapTypes';
+import { BootstrapUser } from 'src/types/bootstrapTypes';
 import { ChartState } from '../explore/types';
 
 export { Dashboard } from 'src/types/Dashboard';
@@ -117,7 +117,7 @@ export type RootState = {
   dataMask: DataMaskStateWithId;
   impressionId: string;
   nativeFilters: NativeFiltersState;
-  user: User;
+  user: BootstrapUser;
 };
 
 /** State of dashboardLayout in redux */
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts b/superset-frontend/src/types/ChartSource.ts
similarity index 91%
rename from superset-frontend/src/dashboard/components/DrillDetailPane/index.ts
rename to superset-frontend/src/types/ChartSource.ts
index 7e23e0a55c..6abc7d754c 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/index.ts
+++ b/superset-frontend/src/types/ChartSource.ts
@@ -17,4 +17,7 @@
  * under the License.
  */
 
-export { default } from './DrillDetailPane';
+export enum ChartSource {
+  Explore = 'explore',
+  Dashboard = 'dashboard',
+}