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',
+}