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

[superset] branch master updated: feat: Implement context menu for drill by (#23454)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9fbfd1c1d8 feat: Implement context menu for drill by (#23454)
9fbfd1c1d8 is described below

commit 9fbfd1c1d883f983ef96b8812297721e2a1a9695
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Wed Mar 29 15:01:51 2023 +0200

    feat: Implement context menu for drill by (#23454)
---
 .../superset-ui-core/src/chart/types/Base.ts       |   6 +
 .../legacy-plugin-chart-world-map/src/WorldMap.js  |   9 +
 .../legacy-plugin-chart-world-map/src/index.js     |   6 +-
 .../plugin-chart-echarts/src/BoxPlot/index.ts      |   8 +-
 .../plugin-chart-echarts/src/Funnel/index.ts       |   6 +-
 .../plugin-chart-echarts/src/Gauge/index.ts        |   6 +-
 .../src/Graph/EchartsGraph.tsx                     |  11 +-
 .../plugin-chart-echarts/src/Graph/index.ts        |   6 +-
 .../src/MixedTimeseries/EchartsMixedTimeseries.tsx |  68 ++++---
 .../src/MixedTimeseries/index.ts                   |   6 +-
 .../plugins/plugin-chart-echarts/src/Pie/index.ts  |   6 +-
 .../plugin-chart-echarts/src/Radar/index.ts        |   6 +-
 .../src/Sunburst/EchartsSunburst.tsx               |  10 +-
 .../plugin-chart-echarts/src/Sunburst/index.ts     |   8 +-
 .../src/Timeseries/Area/index.ts                   |   6 +-
 .../src/Timeseries/EchartsTimeseries.tsx           |  60 +++---
 .../src/Timeseries/Regular/Bar/index.ts            |   6 +-
 .../src/Timeseries/Regular/Line/index.ts           |   6 +-
 .../src/Timeseries/Regular/Scatter/index.ts        |   6 +-
 .../src/Timeseries/Regular/SmoothLine/index.ts     |   6 +-
 .../src/Timeseries/Step/index.ts                   |   6 +-
 .../plugin-chart-echarts/src/Timeseries/index.ts   |   6 +-
 .../src/Treemap/EchartsTreemap.tsx                 |  16 +-
 .../plugin-chart-echarts/src/Treemap/index.ts      |   6 +-
 .../src/utils/eventHandlers.ts                     |   7 +-
 .../src/PivotTableChart.tsx                        |  19 +-
 .../plugin-chart-pivot-table/src/plugin/index.ts   |  10 +-
 .../plugins/plugin-chart-table/src/TableChart.tsx  |  12 ++
 .../plugins/plugin-chart-table/src/index.ts        |   8 +-
 .../src/components/Chart/ChartContextMenu.tsx      |  30 ++-
 .../Chart/DrillBy/DrillByMenuItems.test.tsx        | 190 ++++++++++++++++++
 .../components/Chart/DrillBy/DrillByMenuItems.tsx  | 221 +++++++++++++++++++++
 .../Chart/DrillDetail/DrillDetailMenuItems.tsx     |  34 ++--
 .../components/Chart/MenuItemWithTruncation.tsx    |  58 ++++++
 .../src/components/Chart/utils.test.ts             |   3 +
 superset-frontend/src/components/Chart/utils.ts    |  38 +++-
 .../FiltersConfigForm/ColumnSelect.tsx             |   2 +-
 .../FiltersConfigForm/DatasetSelect.tsx            |   3 +-
 .../FiltersConfigForm/FiltersConfigForm.tsx        |   2 +-
 .../FiltersConfigModal/FiltersConfigForm/utils.ts  |  16 +-
 .../src/dashboard/containers/DashboardPage.tsx     |  14 +-
 superset-frontend/src/dashboard/styles.ts          |   7 +
 .../Chart/utils.ts => utils/cachedSupersetGet.ts}  |  27 +--
 43 files changed, 839 insertions(+), 148 deletions(-)

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 7fa2ba1f77..418d6a36fc 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
@@ -31,6 +31,7 @@ export enum Behavior {
    * when dimensions are right-clicked on.
    */
   DRILL_TO_DETAIL = 'DRILL_TO_DETAIL',
+  DRILL_BY = 'DRILL_BY',
 }
 
 export interface ContextMenuFilters {
@@ -39,6 +40,11 @@ export interface ContextMenuFilters {
     isCurrentValueSelected?: boolean;
   };
   drillToDetail?: BinaryQueryObjectFilterClause[];
+  drillBy?: {
+    filters: BinaryQueryObjectFilterClause[];
+    groupbyFieldName: string;
+    adhocFilterFieldName?: string;
+  };
 }
 
 export enum AppSection {
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 abb9e19b9f..c8aa2cdc2a 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
@@ -172,6 +172,7 @@ function WorldMap(element, props) {
     const val =
       countryFieldtype === 'name' ? mapData[key]?.name : mapData[key]?.country;
     let drillToDetailFilters;
+    let drillByFilters;
     if (val) {
       drillToDetailFilters = [
         {
@@ -181,10 +182,18 @@ function WorldMap(element, props) {
           formattedVal: val,
         },
       ];
+      drillByFilters = [
+        {
+          col: entity,
+          op: '==',
+          val,
+        },
+      ];
     }
     onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
       drillToDetail: drillToDetailFilters,
       crossFilter: getCrossFilterDataMask(source),
+      drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
     });
   };
 
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 8fc0d9aad6..95eb6b59fd 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
@@ -45,7 +45,11 @@ const metadata = new ChartMetadata({
   ],
   thumbnail,
   useLegacyApi: true,
-  behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+  behaviors: [
+    Behavior.INTERACTIVE_CHART,
+    Behavior.DRILL_TO_DETAIL,
+    Behavior.DRILL_BY,
+  ],
 });
 
 export default class WorldMapChartPlugin 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 3c8620e9d8..15d1a4b49e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
+import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
 import buildQuery from './buildQuery';
 import controlPanel from './controlPanel';
 import transformProps from './transformProps';
@@ -44,7 +44,11 @@ export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsBoxPlot'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 742b92151a..39161c8ad6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts
@@ -44,7 +44,11 @@ export default class EchartsFunnelChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsFunnel'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 15de1cd9be..e3036dfbbd 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
@@ -35,7 +35,11 @@ export default class EchartsGaugeChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsGauge'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 47bcb00a3d..8e90bf1791 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx
@@ -137,11 +137,16 @@ export default function EchartsGraph({
         const data = (echartOptions as any).series[0].data as Data;
         const drillToDetailFilters =
           e.dataType === 'node' ? handleNodeClick(data) : handleEdgeClick(data);
+        const node = data.find(item => item.id === e.data.id);
+
         onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
           drillToDetail: drillToDetailFilters,
-          crossFilter: getCrossFilterDataMask(
-            data.find(item => item.id === e.data.id),
-          ),
+          crossFilter: getCrossFilterDataMask(node),
+          drillBy: node && {
+            filters: [{ col: node.col, op: '==', val: node.name }],
+            groupbyFieldName:
+              node.col === formData.source ? 'source' : 'target',
+          },
         });
       }
     },
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 b3bc239d2b..a0275c5bbd 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
@@ -48,7 +48,11 @@ export default class EchartsGraphChartPlugin extends ChartPlugin {
           t('Transformable'),
         ],
         thumbnail,
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
       }),
       transformProps,
     });
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 0018c0e876..02583d4162 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx
@@ -131,42 +131,52 @@ export default function EchartsMixedTimeseries({
         const { data, seriesName, seriesIndex } = eventParams;
         const pointerEvent = eventParams.event.event;
         const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
-        if (data) {
-          const values = [
-            ...(eventParams.name ? [eventParams.name] : []),
-            ...(isFirstQuery(seriesIndex) ? labelMap : labelMapB)[
-              eventParams.seriesName
-            ],
-          ];
-          if (xAxis.type === AxisType.time) {
-            drillToDetailFilters.push({
-              col:
-                xAxis.label === DTTM_ALIAS
-                  ? formData.granularitySqla
-                  : xAxis.label,
-              grain: formData.timeGrainSqla,
-              op: '==',
-              val: data[0],
-              formattedVal: xValueFormatter(data[0]),
-            });
-          }
-          [
-            ...(xAxis.type === AxisType.category ? [xAxis.label] : []),
-            ...(isFirstQuery(seriesIndex)
-              ? formData.groupby
-              : formData.groupbyB),
-          ].forEach((dimension, i) =>
-            drillToDetailFilters.push({
+        const drillByFilters: BinaryQueryObjectFilterClause[] = [];
+        const isFirst = isFirstQuery(seriesIndex);
+        const values = [
+          ...(eventParams.name ? [eventParams.name] : []),
+          ...(isFirst ? labelMap : labelMapB)[eventParams.seriesName],
+        ];
+        if (data && xAxis.type === AxisType.time) {
+          drillToDetailFilters.push({
+            col:
+              xAxis.label === DTTM_ALIAS
+                ? formData.granularitySqla
+                : xAxis.label,
+            grain: formData.timeGrainSqla,
+            op: '==',
+            val: data[0],
+            formattedVal: xValueFormatter(data[0]),
+          });
+        }
+        [
+          ...(data && xAxis.type === AxisType.category ? [xAxis.label] : []),
+          ...(isFirst ? formData.groupby : formData.groupbyB),
+        ].forEach((dimension, i) =>
+          drillToDetailFilters.push({
+            col: dimension,
+            op: '==',
+            val: values[i],
+            formattedVal: String(values[i]),
+          }),
+        );
+
+        [...(isFirst ? formData.groupby : formData.groupbyB)].forEach(
+          (dimension, i) =>
+            drillByFilters.push({
               col: dimension,
               op: '==',
               val: values[i],
-              formattedVal: String(values[i]),
             }),
-          );
-        }
+        );
         onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
           drillToDetail: drillToDetailFilters,
           crossFilter: getCrossFilterDataMask(seriesName, seriesIndex),
+          drillBy: {
+            filters: drillByFilters,
+            groupbyFieldName: isFirst ? 'groupby' : 'groupby_b',
+            adhocFilterFieldName: isFirst ? 'adhoc_filters' : 'adhoc_filters_b',
+          },
         });
       }
     },
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 eb1bef3c9b..66e471235f 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
@@ -54,7 +54,11 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsMixedTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 9f5d61474a..500b40f59f 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,11 @@ export default class EchartsPieChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsPie'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 69f1ee8dac..a544e28b26 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
@@ -46,7 +46,11 @@ export default class EchartsRadarChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsRadar'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         category: t('Ranking'),
         credits: ['https://echarts.apache.org'],
         description: t(
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx
index 5552f7d0ee..7f40574665 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx
@@ -40,7 +40,6 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
     refs,
     emitCrossFilters,
   } = props;
-
   const { columns } = formData;
 
   const getCrossFilterDataMask = useCallback(
@@ -62,7 +61,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
             filters:
               values.length === 0 || !columns
                 ? []
-                : columns.map((col, idx) => {
+                : columns.slice(0, treePath.length).map((col, idx) => {
                     const val = labels.map(v => v[idx]);
                     if (val === null || val === undefined)
                       return {
@@ -111,6 +110,7 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
         const treePath = extractTreePathInfo(eventParams.treePathInfo);
         const pointerEvent = eventParams.event.event;
         const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
+        const drillByFilters: BinaryQueryObjectFilterClause[] = [];
         if (columns?.length) {
           treePath.forEach((path, i) =>
             drillToDetailFilters.push({
@@ -120,10 +120,16 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
               formattedVal: path,
             }),
           );
+          drillByFilters.push({
+            col: columns[treePath.length - 1],
+            op: '==',
+            val: treePath[treePath.length - 1],
+          });
         }
         onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
           drillToDetail: drillToDetailFilters,
           crossFilter: getCrossFilterDataMask(treePathInfo),
+          drillBy: { filters: drillByFilters, groupbyFieldName: 'columns' },
         });
       }
     },
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts
index 5ca8d5a8fc..e6bb0f8b39 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
+import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
 import transformProps from './transformProps';
 import thumbnail from './images/thumbnail.png';
 import controlPanel from './controlPanel';
@@ -31,7 +31,11 @@ export default class EchartsSunburstChartPlugin extends ChartPlugin {
       controlPanel,
       loadChart: () => import('./EchartsSunburst'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         category: t('Part of a Whole'),
         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 b560cf0b4f..733db0a9cb 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,11 @@ export default class EchartsAreaChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 db4f730aff..516255de0c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx
@@ -201,40 +201,48 @@ export default function EchartsTimeseries({
         eventParams.event.stop();
         const { data, seriesName } = eventParams;
         const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
+        const drillByFilters: BinaryQueryObjectFilterClause[] = [];
         const pointerEvent = eventParams.event.event;
         const values = [
           ...(eventParams.name ? [eventParams.name] : []),
-          ...labelMap[eventParams.seriesName],
+          ...labelMap[seriesName],
         ];
-        if (data) {
-          if (xAxis.type === AxisType.time) {
-            drillToDetailFilters.push({
-              col:
-                // if the xAxis is '__timestamp', granularity_sqla will be the column of filter
-                xAxis.label === DTTM_ALIAS
-                  ? formData.granularitySqla
-                  : xAxis.label,
-              grain: formData.timeGrainSqla,
-              op: '==',
-              val: data[0],
-              formattedVal: xValueFormatter(data[0]),
-            });
-          }
-          [
-            ...(xAxis.type === AxisType.category ? [xAxis.label] : []),
-            ...formData.groupby,
-          ].forEach((dimension, i) =>
-            drillToDetailFilters.push({
-              col: dimension,
-              op: '==',
-              val: values[i],
-              formattedVal: String(values[i]),
-            }),
-          );
+        if (data && xAxis.type === AxisType.time) {
+          drillToDetailFilters.push({
+            col:
+              // if the xAxis is '__timestamp', granularity_sqla will be the column of filter
+              xAxis.label === DTTM_ALIAS
+                ? formData.granularitySqla
+                : xAxis.label,
+            grain: formData.timeGrainSqla,
+            op: '==',
+            val: data[0],
+            formattedVal: xValueFormatter(data[0]),
+          });
         }
+        [
+          ...(xAxis.type === AxisType.category && data ? [xAxis.label] : []),
+          ...formData.groupby,
+        ].forEach((dimension, i) =>
+          drillToDetailFilters.push({
+            col: dimension,
+            op: '==',
+            val: values[i],
+            formattedVal: String(values[i]),
+          }),
+        );
+        formData.groupby.forEach((dimension, i) => {
+          drillByFilters.push({
+            col: dimension,
+            op: '==',
+            val: labelMap[seriesName][i],
+          });
+        });
+
         onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
           drillToDetail: drillToDetailFilters,
           crossFilter: getCrossFilterDataMask(seriesName),
+          drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
         });
       }
     },
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 de0050edaa..81f7c15ece 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,11 @@ export default class EchartsTimeseriesBarChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 b6f7f1fceb..cf96910555 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,11 @@ export default class EchartsTimeseriesLineChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 489983cfa6..7fa77763bc 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,11 @@ export default class EchartsTimeseriesScatterChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 ae6dc7ad30..9608407d02 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,11 @@ export default class EchartsTimeseriesSmoothLineChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 3fdeb5aa83..93d439851d 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,11 @@ export default class EchartsTimeseriesStepChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('../EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 c8210cd981..4cf4337fdc 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,11 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsTimeseries'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         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 1ee793cfc7..f9363bd4b6 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx
@@ -116,17 +116,25 @@ export default function EchartsTreemap({
         if (treePath.length > 0) {
           const pointerEvent = eventParams.event.event;
           const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
-          treePath.forEach((path, i) =>
+          const drillByFilters: BinaryQueryObjectFilterClause[] = [];
+          treePath.forEach((path, i) => {
+            const val = path === 'null' ? NULL_STRING : path;
             drillToDetailFilters.push({
               col: groupby[i],
               op: '==',
-              val: path === 'null' ? NULL_STRING : path,
+              val,
               formattedVal: path,
-            }),
-          );
+            });
+            drillByFilters.push({
+              col: groupby[i],
+              op: '==',
+              val,
+            });
+          });
           onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
             drillToDetail: drillToDetailFilters,
             crossFilter: getCrossFilterDataMask(data, treePathInfo),
+            drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
           });
         }
       }
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 ec6d2d3823..9e91965954 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,11 @@ export default class EchartsTreemapChartPlugin extends ChartPlugin<
       controlPanel,
       loadChart: () => import('./EchartsTreemap'),
       metadata: new ChartMetadata({
-        behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+        behaviors: [
+          Behavior.INTERACTIVE_CHART,
+          Behavior.DRILL_TO_DETAIL,
+          Behavior.DRILL_BY,
+        ],
         category: t('Part of a Whole'),
         credits: ['https://echarts.apache.org'],
         description: t(
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 6dafe7ba60..7651bd83bd 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts
@@ -111,11 +111,11 @@ export const contextMenuEventHandler =
     if (onContextMenu) {
       e.event.stop();
       const pointerEvent = e.event.event;
-      const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
+      const drillFilters: BinaryQueryObjectFilterClause[] = [];
       if (groupby.length > 0) {
         const values = labelMap[e.name];
         groupby.forEach((dimension, i) =>
-          drillToDetailFilters.push({
+          drillFilters.push({
             col: dimension,
             op: '==',
             val: values[i],
@@ -124,8 +124,9 @@ export const contextMenuEventHandler =
         );
       }
       onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
-        drillToDetail: drillToDetailFilters,
+        drillToDetail: drillFilters,
         crossFilter: getCrossFilterDataMask(e.name),
+        drillBy: { filters: drillFilters, groupbyFieldName: 'groupby' },
       });
     }
   };
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 3bbf8af9a0..8ec1cb9ad1 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx
@@ -478,10 +478,27 @@ export default function PivotTableChart(props: PivotTableProps) {
         onContextMenu(e.clientX, e.clientY, {
           drillToDetail: drillToDetailFilters,
           crossFilter: getCrossFilterDataMask(dataPoint),
+          drillBy: dataPoint && {
+            filters: [
+              {
+                col: Object.keys(dataPoint)[0],
+                op: '==',
+                val: Object.values(dataPoint)[0],
+              },
+            ],
+            groupbyFieldName: rowKey ? 'groupbyRows' : 'groupbyColumns',
+          },
         });
       }
     },
-    [cols, dateFormatters, onContextMenu, rows, timeGrainSqla],
+    [
+      cols,
+      dateFormatters,
+      getCrossFilterDataMask,
+      onContextMenu,
+      rows,
+      timeGrainSqla,
+    ],
   );
 
   return (
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 b2d355f0ff..6963c67f0d 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
@@ -17,12 +17,12 @@
  * under the License.
  */
 import {
-  t,
+  Behavior,
   ChartMetadata,
   ChartPlugin,
-  Behavior,
   ChartProps,
   QueryFormData,
+  t,
 } from '@superset-ui/core';
 import buildQuery from './buildQuery';
 import controlPanel from './controlPanel';
@@ -47,7 +47,11 @@ export default class PivotTableChartPlugin extends ChartPlugin<
    */
   constructor() {
     const metadata = new ChartMetadata({
-      behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+      behaviors: [
+        Behavior.INTERACTIVE_CHART,
+        Behavior.DRILL_TO_DETAIL,
+        Behavior.DRILL_BY,
+      ],
       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-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index ac02a10137..fd5ce8aa3e 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -391,6 +391,18 @@ export default function TableChart<D extends DataRecord = DataRecord>(
             crossFilter: cellPoint.isMetric
               ? undefined
               : getCrossFilterDataMask(cellPoint.key, cellPoint.value),
+            drillBy: cellPoint.isMetric
+              ? undefined
+              : {
+                  filters: [
+                    {
+                      col: cellPoint.key,
+                      op: '==',
+                      val: cellPoint.value as string | number | boolean,
+                    },
+                  ],
+                  groupbyFieldName: 'groupby',
+                },
           });
         }
       : undefined;
diff --git a/superset-frontend/plugins/plugin-chart-table/src/index.ts b/superset-frontend/plugins/plugin-chart-table/src/index.ts
index 4e862fc5a5..f6ce3b484e 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/index.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/index.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
+import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
 import transformProps from './transformProps';
 import thumbnail from './images/thumbnail.png';
 import example1 from './images/Table.jpg';
@@ -31,7 +31,11 @@ export { default as __hack__ } from './types';
 export * from './types';
 
 const metadata = new ChartMetadata({
-  behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
+  behaviors: [
+    Behavior.INTERACTIVE_CHART,
+    Behavior.DRILL_TO_DETAIL,
+    Behavior.DRILL_BY,
+  ],
   category: t('Table'),
   canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
   description: t(
diff --git a/superset-frontend/src/components/Chart/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu.tsx
index 0f0082aee9..3ddbc91b22 100644
--- a/superset-frontend/src/components/Chart/ChartContextMenu.tsx
+++ b/superset-frontend/src/components/Chart/ChartContextMenu.tsx
@@ -44,6 +44,7 @@ import { DrillDetailMenuItems } from './DrillDetail';
 import { getMenuAdjustedY } from './utils';
 import { updateDataMask } from '../../dataMask/actions';
 import { MenuItemTooltip } from './DisabledMenuItemTooltip';
+import { DrillByMenuItems } from './DrillBy/DrillByMenuItems';
 
 export interface ChartContextMenuProps {
   id: number;
@@ -84,17 +85,25 @@ const ChartContextMenu = (
   const showDrillToDetail =
     isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) && canExplore;
 
+  const showDrillBy = isFeatureEnabled(FeatureFlag.DRILL_BY) && canExplore;
+
+  const showCrossFilters = isFeatureEnabled(
+    FeatureFlag.DASHBOARD_CROSS_FILTERS,
+  );
   const isCrossFilteringSupportedByChart = getChartMetadataRegistry()
     .get(formData.viz_type)
     ?.behaviors?.includes(Behavior.INTERACTIVE_CHART);
 
   let itemsCount = 0;
-  if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) {
+  if (showCrossFilters) {
     itemsCount += 1;
   }
   if (showDrillToDetail) {
     itemsCount += 2; // Drill to detail always has 2 top-level menu items
   }
+  if (showDrillBy) {
+    itemsCount += 1;
+  }
   if (itemsCount === 0) {
     itemsCount = 1; // "No actions" appears if no actions in menu
   }
@@ -180,6 +189,25 @@ const ChartContextMenu = (
         isContextMenu
         contextMenuY={clientY}
         onSelection={onSelection}
+        submenuIndex={showCrossFilters ? 2 : 1}
+      />,
+    );
+  }
+  if (showDrillBy) {
+    let submenuIndex = 0;
+    if (showCrossFilters) {
+      submenuIndex += 1;
+    }
+    if (showDrillToDetail) {
+      submenuIndex += 2;
+    }
+    menuItems.push(
+      <DrillByMenuItems
+        filters={filters?.drillBy?.filters}
+        groupbyFieldName={filters?.drillBy?.groupbyFieldName}
+        formData={formData}
+        contextMenuY={clientY}
+        submenuIndex={submenuIndex}
       />,
     );
   }
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
new file mode 100644
index 0000000000..e7db5efe8e
--- /dev/null
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx
@@ -0,0 +1,190 @@
+/**
+ * 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 {
+  Behavior,
+  ChartMetadata,
+  getChartMetadataRegistry,
+} from '@superset-ui/core';
+import fetchMock from 'fetch-mock';
+import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
+import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
+import { Menu } from 'src/components/Menu';
+import { supersetGetCache } from 'src/utils/cachedSupersetGet';
+import { DrillByMenuItems, DrillByMenuItemsProps } from './DrillByMenuItems';
+
+/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
+
+const datasetEndpointMatcher = 'glob:*/api/v1/dataset/7';
+const { form_data: defaultFormData } = chartQueries[sliceId];
+
+const defaultColumns = [
+  { column_name: 'col1', groupby: true },
+  { column_name: 'col2', groupby: true },
+  { column_name: 'col3', groupby: true },
+  { column_name: 'col4', groupby: true },
+  { column_name: 'col5', groupby: true },
+  { column_name: 'col6', groupby: true },
+  { column_name: 'col7', groupby: true },
+  { column_name: 'col8', groupby: true },
+  { column_name: 'col9', groupby: true },
+  { column_name: 'col10', groupby: true },
+  { column_name: 'col11', groupby: true },
+];
+
+const defaultFilters = [
+  {
+    col: 'filter_col',
+    op: '==' as const,
+    val: 'val',
+  },
+];
+
+const renderMenu = ({
+  formData = defaultFormData,
+  filters = defaultFilters,
+}: Partial<DrillByMenuItemsProps>) =>
+  render(
+    <Menu>
+      <DrillByMenuItems
+        formData={formData ?? defaultFormData}
+        filters={filters}
+        groupbyFieldName="groupby"
+      />
+    </Menu>,
+    { useRouter: true, useRedux: true },
+  );
+
+const expectDrillByDisabled = async (tooltipContent: string) => {
+  const drillByMenuItem = screen.getByRole('menuitem', {
+    name: 'Drill by',
+  });
+
+  expect(drillByMenuItem).toBeVisible();
+  expect(drillByMenuItem).toHaveAttribute('aria-disabled', 'true');
+  const tooltipTrigger = within(drillByMenuItem).getByTestId('tooltip-trigger');
+  userEvent.hover(tooltipTrigger as HTMLElement);
+  const tooltip = await screen.findByRole('tooltip', { name: tooltipContent });
+
+  expect(tooltip).toBeInTheDocument();
+};
+
+const expectDrillByEnabled = async () => {
+  const drillByMenuItem = screen.getByRole('menuitem', {
+    name: 'Drill by',
+  });
+  expect(drillByMenuItem).toBeInTheDocument();
+  await waitFor(() =>
+    expect(drillByMenuItem).not.toHaveAttribute('aria-disabled'),
+  );
+  const tooltipTrigger =
+    within(drillByMenuItem).queryByTestId('tooltip-trigger');
+  expect(tooltipTrigger).not.toBeInTheDocument();
+
+  userEvent.hover(
+    within(drillByMenuItem).getByRole('button', { name: 'Drill by' }),
+  );
+  expect(await screen.findByTestId('drill-by-submenu')).toBeInTheDocument();
+};
+
+getChartMetadataRegistry().registerValue(
+  'pie',
+  new ChartMetadata({
+    name: 'fake pie',
+    thumbnail: '.png',
+    useLegacyApi: false,
+    behaviors: [Behavior.DRILL_BY],
+  }),
+);
+
+describe('Drill by menu items', () => {
+  afterEach(() => {
+    supersetGetCache.clear();
+    fetchMock.restore();
+  });
+
+  test('render disabled menu item for unsupported chart', async () => {
+    renderMenu({
+      formData: { ...defaultFormData, viz_type: 'unsupported_viz' },
+    });
+    await expectDrillByDisabled(
+      'Drill by is not yet supported for this chart type',
+    );
+  });
+
+  test('render disabled menu item for supported chart, no filters', async () => {
+    renderMenu({ filters: [] });
+    await expectDrillByDisabled(
+      'Drill by is not available for this data point',
+    );
+  });
+
+  test('render disabled menu item for supported chart, no columns', async () => {
+    fetchMock.get(datasetEndpointMatcher, { result: { columns: [] } });
+    renderMenu({});
+    await waitFor(() => fetchMock.called(datasetEndpointMatcher));
+    await expectDrillByDisabled('No dimensions available for drill by');
+  });
+
+  test('render menu item with submenu without searchbox', async () => {
+    const slicedColumns = defaultColumns.slice(0, 9);
+    fetchMock.get(datasetEndpointMatcher, {
+      result: { columns: slicedColumns },
+    });
+    renderMenu({});
+    await waitFor(() => fetchMock.called(datasetEndpointMatcher));
+    await expectDrillByEnabled();
+    slicedColumns.forEach(column => {
+      expect(screen.getByText(column.column_name)).toBeInTheDocument();
+    });
+    expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
+  });
+
+  test('render menu item with submenu and searchbox', async () => {
+    fetchMock.get(datasetEndpointMatcher, {
+      result: { columns: defaultColumns },
+    });
+    renderMenu({});
+    await waitFor(() => fetchMock.called(datasetEndpointMatcher));
+    await expectDrillByEnabled();
+    defaultColumns.forEach(column => {
+      expect(screen.getByText(column.column_name)).toBeInTheDocument();
+    });
+
+    const searchbox = screen.getByRole('textbox');
+    expect(searchbox).toBeInTheDocument();
+
+    userEvent.type(searchbox, 'col1');
+
+    await screen.findByText('col1');
+
+    const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
+
+    defaultColumns
+      .filter(col => !expectedFilteredColumnNames.includes(col.column_name))
+      .forEach(col => {
+        expect(screen.queryByText(col.column_name)).not.toBeInTheDocument();
+      });
+
+    expectedFilteredColumnNames.forEach(colName => {
+      expect(screen.getByText(colName)).toBeInTheDocument();
+    });
+  });
+});
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
new file mode 100644
index 0000000000..1da50a412f
--- /dev/null
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx
@@ -0,0 +1,221 @@
+/**
+ * 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, {
+  ChangeEvent,
+  ReactNode,
+  useCallback,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react';
+import { Menu } from 'src/components/Menu';
+import {
+  BaseFormData,
+  Behavior,
+  BinaryQueryObjectFilterClause,
+  Column,
+  css,
+  ensureIsArray,
+  getChartMetadataRegistry,
+  t,
+  useTheme,
+} from '@superset-ui/core';
+import Icons from 'src/components/Icons';
+import { Input } from 'src/components/Input';
+import {
+  cachedSupersetGet,
+  supersetGetCache,
+} from 'src/utils/cachedSupersetGet';
+import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
+import { getSubmenuYOffset } from '../utils';
+import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
+
+const MAX_SUBMENU_HEIGHT = 200;
+const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
+const SEARCH_INPUT_HEIGHT = 48;
+
+export interface DrillByMenuItemsProps {
+  filters?: BinaryQueryObjectFilterClause[];
+  formData: BaseFormData & { [key: string]: any };
+  contextMenuY?: number;
+  submenuIndex?: number;
+  groupbyFieldName?: string;
+}
+export const DrillByMenuItems = ({
+  filters,
+  groupbyFieldName,
+  formData,
+  contextMenuY = 0,
+  submenuIndex = 0,
+  ...rest
+}: DrillByMenuItemsProps) => {
+  const theme = useTheme();
+  const [searchInput, setSearchInput] = useState('');
+  const [columns, setColumns] = useState<Column[]>([]);
+  useEffect(() => {
+    // Input is displayed only when columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
+    // Reset search input in case Input gets removed
+    setSearchInput('');
+  }, [columns.length]);
+
+  const hasDrillBy = ensureIsArray(filters).length && groupbyFieldName;
+
+  const handlesDimensionContextMenu = useMemo(
+    () =>
+      getChartMetadataRegistry()
+        .get(formData.viz_type)
+        ?.behaviors.find(behavior => behavior === Behavior.DRILL_BY),
+    [formData.viz_type],
+  );
+
+  useEffect(() => {
+    if (handlesDimensionContextMenu && hasDrillBy) {
+      const datasetId = formData.datasource.split('__')[0];
+      cachedSupersetGet({
+        endpoint: `/api/v1/dataset/${datasetId}`,
+      })
+        .then(({ json: { result } }) => {
+          setColumns(
+            ensureIsArray(result.columns)
+              .filter(column => column.groupby)
+              .filter(
+                column =>
+                  !ensureIsArray(formData[groupbyFieldName]).includes(
+                    column.column_name,
+                  ),
+              ),
+          );
+        })
+        .catch(() => {
+          supersetGetCache.delete(`/api/v1/dataset/${datasetId}`);
+        });
+    }
+  }, [formData, groupbyFieldName, handlesDimensionContextMenu, hasDrillBy]);
+
+  const handleInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {
+    e.stopPropagation();
+    const input = e?.target?.value;
+    setSearchInput(input);
+  }, []);
+
+  const filteredColumns = useMemo(
+    () =>
+      columns.filter(column =>
+        (column.verbose_name || column.column_name)
+          .toLowerCase()
+          .includes(searchInput.toLowerCase()),
+      ),
+    [columns, searchInput],
+  );
+
+  const submenuYOffset = useMemo(
+    () =>
+      getSubmenuYOffset(
+        contextMenuY,
+        filteredColumns.length || 1,
+        submenuIndex,
+        MAX_SUBMENU_HEIGHT,
+        columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD
+          ? SEARCH_INPUT_HEIGHT
+          : 0,
+      ),
+    [contextMenuY, filteredColumns.length, submenuIndex, columns.length],
+  );
+
+  let tooltip: ReactNode;
+
+  if (!handlesDimensionContextMenu) {
+    tooltip = t('Drill by is not yet supported for this chart type');
+  } else if (!hasDrillBy) {
+    tooltip = t('Drill by is not available for this data point');
+  } else if (columns.length === 0) {
+    tooltip = t('No dimensions available for drill by');
+  }
+
+  if (!handlesDimensionContextMenu || !hasDrillBy || columns.length === 0) {
+    return (
+      <Menu.Item key="drill-by-disabled" disabled {...rest}>
+        <div>
+          {t('Drill by')}
+          <MenuItemTooltip title={tooltip} />
+        </div>
+      </Menu.Item>
+    );
+  }
+
+  return (
+    <Menu.SubMenu
+      title={t('Drill by')}
+      key="drill-by-submenu"
+      popupClassName="chart-context-submenu"
+      popupOffset={[0, submenuYOffset]}
+      {...rest}
+    >
+      <div data-test="drill-by-submenu">
+        {columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD && (
+          <Input
+            prefix={
+              <Icons.Search
+                iconSize="l"
+                iconColor={theme.colors.grayscale.light1}
+              />
+            }
+            onChange={handleInput}
+            placeholder={t('Search columns')}
+            value={searchInput}
+            onClick={e => {
+              // prevent closing menu when clicking on input
+              e.nativeEvent.stopImmediatePropagation();
+            }}
+            allowClear
+            css={css`
+              width: auto;
+              max-width: 100%;
+              margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
+              box-shadow: none;
+            `}
+          />
+        )}
+        {filteredColumns.length ? (
+          <div
+            css={css`
+              max-height: ${MAX_SUBMENU_HEIGHT}px;
+              overflow: auto;
+            `}
+          >
+            {filteredColumns.map(column => (
+              <MenuItemWithTruncation
+                key={`drill-by-item-${column.column_name}`}
+                tooltipText={column.verbose_name || column.column_name}
+                {...rest}
+              >
+                {column.verbose_name || column.column_name}
+              </MenuItemWithTruncation>
+            ))}
+          </div>
+        ) : (
+          <Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
+            {t('No columns found')}
+          </Menu.Item>
+        )}
+      </div>
+    </Menu.SubMenu>
+  );
+};
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
index b3daada2d4..98fe90eafa 100644
--- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
@@ -31,10 +31,10 @@ import {
 } from '@superset-ui/core';
 import { Menu } from 'src/components/Menu';
 import DrillDetailModal from './DrillDetailModal';
-import { getMenuAdjustedY, MENU_ITEM_HEIGHT } from '../utils';
+import { getSubmenuYOffset } from '../utils';
 import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
+import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
 
-const MENU_PADDING = 4;
 const DRILL_TO_DETAIL_TEXT = t('Drill to detail by');
 
 const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
@@ -65,6 +65,7 @@ export type DrillDetailMenuItemsProps = {
   contextMenuY?: number;
   onSelection?: () => void;
   onClick?: (event: MouseEvent) => void;
+  submenuIndex?: number;
 };
 
 const DrillDetailMenuItems = ({
@@ -75,6 +76,7 @@ const DrillDetailMenuItems = ({
   contextMenuY = 0,
   onSelection = () => null,
   onClick = () => null,
+  submenuIndex = 0,
   ...props
 }: DrillDetailMenuItemsProps) => {
   const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
@@ -162,31 +164,35 @@ const DrillDetailMenuItems = ({
   }
 
   // Ensure submenu doesn't appear offscreen
-  const submenuYOffset = useMemo(() => {
-    const itemsCount = filters.length > 1 ? filters.length + 1 : filters.length;
-    const submenuY =
-      contextMenuY + MENU_PADDING + MENU_ITEM_HEIGHT + MENU_PADDING;
-
-    return getMenuAdjustedY(submenuY, itemsCount) - submenuY;
-  }, [contextMenuY, filters.length]);
+  const submenuYOffset = useMemo(
+    () =>
+      getSubmenuYOffset(
+        contextMenuY,
+        filters.length > 1 ? filters.length + 1 : filters.length,
+        submenuIndex,
+      ),
+    [contextMenuY, filters.length, submenuIndex],
+  );
 
   if (handlesDimensionContextMenu && !noAggregations && filters?.length) {
     drillToDetailByMenuItem = (
       <Menu.SubMenu
         {...props}
         popupOffset={[0, submenuYOffset]}
+        popupClassName="chart-context-submenu"
         title={DRILL_TO_DETAIL_TEXT}
       >
         <div data-test="drill-to-detail-by-submenu">
           {filters.map((filter, i) => (
-            <Menu.Item
+            <MenuItemWithTruncation
               {...props}
+              tooltipText={`${DRILL_TO_DETAIL_TEXT} ${filter.formattedVal}`}
               key={`drill-detail-filter-${i}`}
               onClick={openModal.bind(null, [filter])}
             >
               {`${DRILL_TO_DETAIL_TEXT} `}
               <Filter>{filter.formattedVal}</Filter>
-            </Menu.Item>
+            </MenuItemWithTruncation>
           ))}
           {filters.length > 1 && (
             <Menu.Item
@@ -194,8 +200,10 @@ const DrillDetailMenuItems = ({
               key="drill-detail-filter-all"
               onClick={openModal.bind(null, filters)}
             >
-              {`${DRILL_TO_DETAIL_TEXT} `}
-              <Filter>{t('all')}</Filter>
+              <div>
+                {`${DRILL_TO_DETAIL_TEXT} `}
+                <Filter>{t('all')}</Filter>
+              </div>
             </Menu.Item>
           )}
         </div>
diff --git a/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx
new file mode 100644
index 0000000000..24c58d64bc
--- /dev/null
+++ b/superset-frontend/src/components/Chart/MenuItemWithTruncation.tsx
@@ -0,0 +1,58 @@
+/**
+ * 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 } from 'react';
+import { css, truncationCSS, useCSSTextTruncation } from '@superset-ui/core';
+import { Menu } from 'src/components/Menu';
+import { Tooltip } from 'src/components/Tooltip';
+
+export type MenuItemWithTruncationProps = {
+  tooltipText: ReactNode;
+  children: ReactNode;
+  onClick?: () => void;
+};
+
+export const MenuItemWithTruncation = ({
+  tooltipText,
+  children,
+  ...props
+}: MenuItemWithTruncationProps) => {
+  const [itemRef, itemIsTruncated] = useCSSTextTruncation<HTMLDivElement>();
+
+  return (
+    <Menu.Item
+      css={css`
+        display: flex;
+      `}
+      {...props}
+    >
+      <Tooltip title={itemIsTruncated ? tooltipText : null}>
+        <div
+          ref={itemRef}
+          css={css`
+            max-width: 100%;
+            ${truncationCSS};
+          `}
+        >
+          {children}
+        </div>
+      </Tooltip>
+    </Menu.Item>
+  );
+};
diff --git a/superset-frontend/src/components/Chart/utils.test.ts b/superset-frontend/src/components/Chart/utils.test.ts
index b8de155a2a..192905e69d 100644
--- a/superset-frontend/src/components/Chart/utils.test.ts
+++ b/superset-frontend/src/components/Chart/utils.test.ts
@@ -39,4 +39,7 @@ test('correctly positions at lower edge of screen', () => {
   expect(getMenuAdjustedY(425, 1)).toEqual(425); // No adjustment
   expect(getMenuAdjustedY(425, 2)).toEqual(404); // Adjustment
   expect(getMenuAdjustedY(425, 3)).toEqual(372); // Adjustment
+
+  expect(getMenuAdjustedY(425, 8, 200)).toEqual(268);
+  expect(getMenuAdjustedY(425, 8, 200, 48)).toEqual(220);
 });
diff --git a/superset-frontend/src/components/Chart/utils.ts b/superset-frontend/src/components/Chart/utils.ts
index 54fc5e8926..6dd089f8a1 100644
--- a/superset-frontend/src/components/Chart/utils.ts
+++ b/superset-frontend/src/components/Chart/utils.ts
@@ -18,6 +18,7 @@
  */
 
 export const MENU_ITEM_HEIGHT = 32;
+const MENU_PADDING = 4;
 const MENU_VERTICAL_SPACING = 32;
 
 /**
@@ -27,14 +28,45 @@ const MENU_VERTICAL_SPACING = 32;
  * @param clientY The original Y-offset
  * @param itemsCount The number of menu items
  */
-export function getMenuAdjustedY(clientY: number, itemsCount: number) {
+export const getMenuAdjustedY = (
+  clientY: number,
+  itemsCount: number,
+  maxItemsContainerHeight = Number.MAX_SAFE_INTEGER,
+  additionalItemsHeight = 0,
+) => {
   // Viewport height
   const vh = Math.max(
     document.documentElement.clientHeight || 0,
     window.innerHeight || 0,
   );
 
-  const menuHeight = MENU_ITEM_HEIGHT * itemsCount + MENU_VERTICAL_SPACING;
+  const menuHeight =
+    Math.min(MENU_ITEM_HEIGHT * itemsCount, maxItemsContainerHeight) +
+    MENU_VERTICAL_SPACING +
+    additionalItemsHeight;
   // Always show the context menu inside the viewport
   return vh - clientY < menuHeight ? vh - menuHeight : clientY;
-}
+};
+
+export const getSubmenuYOffset = (
+  contextMenuY: number,
+  itemsCount: number,
+  submenuIndex = 0,
+  maxItemsContainerHeight = Number.MAX_SAFE_INTEGER,
+  additionalItemsHeight = 0,
+) => {
+  const submenuY =
+    contextMenuY +
+    MENU_PADDING +
+    MENU_ITEM_HEIGHT * submenuIndex +
+    MENU_PADDING;
+
+  return (
+    getMenuAdjustedY(
+      submenuY,
+      itemsCount,
+      maxItemsContainerHeight,
+      additionalItemsHeight,
+    ) - submenuY
+  );
+};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
index fdd7811df2..85fe8af34e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx
@@ -22,8 +22,8 @@ import { Column, ensureIsArray, t, useChangeEffect } from '@superset-ui/core';
 import { Select, FormInstance } from 'src/components';
 import { useToasts } from 'src/components/MessageToasts/withToasts';
 import { getClientErrorObject } from 'src/utils/getClientErrorObject';
+import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
 import { NativeFiltersForm } from '../types';
-import { cachedSupersetGet } from './utils';
 
 interface ColumnSelectProps {
   allowClear?: boolean;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
index db1c6d4124..e5e62b008e 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/DatasetSelect.tsx
@@ -24,7 +24,8 @@ import {
   ClientErrorObject,
   getClientErrorObject,
 } from 'src/utils/getClientErrorObject';
-import { cachedSupersetGet, datasetToSelectOption } from './utils';
+import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
+import { datasetToSelectOption } from './utils';
 
 interface DatasetSelectProps {
   onChange: (value: { label: string; value: number }) => void;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index c577eb9213..1abc121b86 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -60,6 +60,7 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
 import { Radio } from 'src/components/Radio';
 import Tabs from 'src/components/Tabs';
 import { Tooltip } from 'src/components/Tooltip';
+import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
 import {
   Chart,
   ChartsState,
@@ -90,7 +91,6 @@ import getControlItemsMap from './getControlItemsMap';
 import RemovedFilter from './RemovedFilter';
 import { useBackendFormUpdate, useDefaultValue } from './state';
 import {
-  cachedSupersetGet,
   hasTemporalColumns,
   mostUsedDataset,
   setNativeFilterFieldValues,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
index de48dbf553..92185039d8 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/utils.ts
@@ -20,14 +20,8 @@ import { flatMapDeep } from 'lodash';
 import { FormInstance } from 'src/components';
 import React from 'react';
 import { CustomControlItem, Dataset } from '@superset-ui/chart-controls';
-import {
-  Column,
-  ensureIsArray,
-  GenericDataType,
-  SupersetClient,
-} from '@superset-ui/core';
+import { Column, ensureIsArray, GenericDataType } from '@superset-ui/core';
 import { DatasourcesState, ChartsState } from 'src/dashboard/types';
-import { cacheWrapper } from 'src/utils/cacheWrapper';
 import { FILTER_SUPPORTED_TYPES } from './constants';
 
 const FILTERS_FIELD_NAME = 'filters';
@@ -124,11 +118,3 @@ export const mostUsedDataset = (
 
   return datasets[mostUsedDataset]?.id;
 };
-
-const localCache = new Map<string, any>();
-
-export const cachedSupersetGet = cacheWrapper(
-  SupersetClient.get,
-  localCache,
-  ({ endpoint }) => endpoint || '',
-);
diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
index 7323f120c9..79b242a179 100644
--- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx
+++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
@@ -59,7 +59,11 @@ import { DashboardContextForExplore } from 'src/types/DashboardContextForExplore
 import shortid from 'shortid';
 import { RootState } from '../types';
 import { getActiveFilters } from '../util/activeDashboardFilters';
-import { filterCardPopoverStyle, headerStyles } from '../styles';
+import {
+  chartContextMenuStyles,
+  filterCardPopoverStyle,
+  headerStyles,
+} from '../styles';
 
 export const DashboardPageIdContext = React.createContext('');
 
@@ -279,7 +283,13 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
 
   return (
     <>
-      <Global styles={[filterCardPopoverStyle(theme), headerStyles(theme)]} />
+      <Global
+        styles={[
+          filterCardPopoverStyle(theme),
+          headerStyles(theme),
+          chartContextMenuStyles(theme),
+        ]}
+      />
       <DashboardPageIdContext.Provider value={dashboardPageId}>
         <DashboardContainer />
       </DashboardPageIdContext.Provider>
diff --git a/superset-frontend/src/dashboard/styles.ts b/superset-frontend/src/dashboard/styles.ts
index a5f49acb85..5d431f50b6 100644
--- a/superset-frontend/src/dashboard/styles.ts
+++ b/superset-frontend/src/dashboard/styles.ts
@@ -87,3 +87,10 @@ export const filterCardPopoverStyle = (theme: SupersetTheme) => css`
     }
   }
 `;
+
+export const chartContextMenuStyles = (theme: SupersetTheme) => css`
+  .ant-dropdown-menu-submenu.chart-context-submenu {
+    max-width: ${theme.gridUnit * 60}px;
+    min-width: ${theme.gridUnit * 40}px;
+  }
+`;
diff --git a/superset-frontend/src/components/Chart/utils.ts b/superset-frontend/src/utils/cachedSupersetGet.ts
similarity index 54%
copy from superset-frontend/src/components/Chart/utils.ts
copy to superset-frontend/src/utils/cachedSupersetGet.ts
index 54fc5e8926..2f319cac35 100644
--- a/superset-frontend/src/components/Chart/utils.ts
+++ b/superset-frontend/src/utils/cachedSupersetGet.ts
@@ -17,24 +17,13 @@
  * under the License.
  */
 
-export const MENU_ITEM_HEIGHT = 32;
-const MENU_VERTICAL_SPACING = 32;
+import { SupersetClient } from '@superset-ui/core';
+import { cacheWrapper } from './cacheWrapper';
 
-/**
- * Calculates an adjusted Y-offset for a menu or submenu to prevent that
- * menu from appearing offscreen
- *
- * @param clientY The original Y-offset
- * @param itemsCount The number of menu items
- */
-export function getMenuAdjustedY(clientY: number, itemsCount: number) {
-  // Viewport height
-  const vh = Math.max(
-    document.documentElement.clientHeight || 0,
-    window.innerHeight || 0,
-  );
+export const supersetGetCache = new Map<string, any>();
 
-  const menuHeight = MENU_ITEM_HEIGHT * itemsCount + MENU_VERTICAL_SPACING;
-  // Always show the context menu inside the viewport
-  return vh - clientY < menuHeight ? vh - menuHeight : clientY;
-}
+export const cachedSupersetGet = cacheWrapper(
+  SupersetClient.get,
+  supersetGetCache,
+  ({ endpoint }) => endpoint || '',
+);