mirror of https://github.com/apache/superset.git
feat(dashboard): menu improvements, fallback support for Drill to Detail (#21351)
This commit is contained in:
parent
54f6fd6a82
commit
76e57ec651
|
@ -43,13 +43,29 @@ function openModalFromChartContext(targetMenuItem: string) {
|
||||||
interceptSamples();
|
interceptSamples();
|
||||||
|
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.get('.ant-dropdown')
|
if (targetMenuItem.startsWith('Drill to detail by')) {
|
||||||
.not('.ant-dropdown-hidden')
|
cy.get('.ant-dropdown')
|
||||||
.first()
|
.not('.ant-dropdown-hidden')
|
||||||
.find("[role='menu'] [role='menuitem']")
|
.first()
|
||||||
.contains(targetMenuItem)
|
.find("[role='menu'] [role='menuitem'] [title='Drill to detail by']")
|
||||||
.first()
|
.trigger('mouseover');
|
||||||
.click();
|
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');
|
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', () => {
|
describe('Tier 2 charts', () => {
|
||||||
|
|
|
@ -25,6 +25,12 @@ export type HandlerFunction = (...args: unknown[]) => void;
|
||||||
export enum Behavior {
|
export enum Behavior {
|
||||||
INTERACTIVE_CHART = 'INTERACTIVE_CHART',
|
INTERACTIVE_CHART = 'INTERACTIVE_CHART',
|
||||||
NATIVE_FILTER = 'NATIVE_FILTER',
|
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 {
|
export enum AppSection {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export {
|
||||||
isXAxisSet,
|
isXAxisSet,
|
||||||
hasGenericChartAxes,
|
hasGenericChartAxes,
|
||||||
} from './getXAxis';
|
} from './getXAxis';
|
||||||
|
export { default as extractQueryFields } from './extractQueryFields';
|
||||||
|
|
||||||
export * from './types/AnnotationLayer';
|
export * from './types/AnnotationLayer';
|
||||||
export * from './types/QueryFormData';
|
export * from './types/QueryFormData';
|
||||||
|
|
|
@ -121,7 +121,7 @@ function WorldMap(element, props) {
|
||||||
formattedVal: val,
|
formattedVal: val,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||||
} else {
|
} else {
|
||||||
logging.warn(
|
logging.warn(
|
||||||
t(
|
t(
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 transformProps from './transformProps';
|
||||||
import thumbnail from './images/thumbnail.png';
|
import thumbnail from './images/thumbnail.png';
|
||||||
import example1 from './images/WorldMap1.jpg';
|
import example1 from './images/WorldMap1.jpg';
|
||||||
|
@ -45,6 +45,7 @@ const metadata = new ChartMetadata({
|
||||||
],
|
],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
useLegacyApi: true,
|
useLegacyApi: true,
|
||||||
|
behaviors: [Behavior.DRILL_TO_DETAIL],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class WorldMapChartPlugin extends ChartPlugin {
|
export default class WorldMapChartPlugin extends ChartPlugin {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 controlPanel from './controlPanel';
|
||||||
import transformProps from './transformProps';
|
import transformProps from './transformProps';
|
||||||
import buildQuery from './buildQuery';
|
import buildQuery from './buildQuery';
|
||||||
|
@ -46,6 +46,7 @@ const metadata = new ChartMetadata({
|
||||||
t('Description'),
|
t('Description'),
|
||||||
],
|
],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
behaviors: [Behavior.DRILL_TO_DETAIL],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class BigNumberTotalChartPlugin extends ChartPlugin<
|
export default class BigNumberTotalChartPlugin extends ChartPlugin<
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
computeMaxFontSize,
|
computeMaxFontSize,
|
||||||
BRAND_COLOR,
|
BRAND_COLOR,
|
||||||
styled,
|
styled,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { EChartsCoreOption } from 'echarts';
|
import { EChartsCoreOption } from 'echarts';
|
||||||
import Echart from '../components/Echart';
|
import Echart from '../components/Echart';
|
||||||
|
@ -65,9 +65,9 @@ type BigNumberVisProps = {
|
||||||
mainColor: string;
|
mainColor: string;
|
||||||
echartOptions: EChartsCoreOption;
|
echartOptions: EChartsCoreOption;
|
||||||
onContextMenu?: (
|
onContextMenu?: (
|
||||||
filters: QueryObjectFilterClause[],
|
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
) => void;
|
) => void;
|
||||||
xValueFormatter?: TimeFormatter;
|
xValueFormatter?: TimeFormatter;
|
||||||
formData?: BigNumberWithTrendlineFormData;
|
formData?: BigNumberWithTrendlineFormData;
|
||||||
|
@ -171,11 +171,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
|
||||||
const onContextMenu = (e: MouseEvent<HTMLDivElement>) => {
|
const onContextMenu = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
if (this.props.onContextMenu) {
|
if (this.props.onContextMenu) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onContextMenu(
|
this.props.onContextMenu(e.nativeEvent.clientX, e.nativeEvent.clientY);
|
||||||
[],
|
|
||||||
e.nativeEvent.clientX,
|
|
||||||
e.nativeEvent.clientY,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -249,7 +245,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
|
||||||
const { data } = eventParams;
|
const { data } = eventParams;
|
||||||
if (data) {
|
if (data) {
|
||||||
const pointerEvent = eventParams.event.event;
|
const pointerEvent = eventParams.event.event;
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
filters.push({
|
filters.push({
|
||||||
col: this.props.formData?.granularitySqla,
|
col: this.props.formData?.granularitySqla,
|
||||||
grain: this.props.formData?.timeGrainSqla,
|
grain: this.props.formData?.timeGrainSqla,
|
||||||
|
@ -258,9 +254,9 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
|
||||||
formattedVal: this.props.xValueFormatter?.(data[0]),
|
formattedVal: this.props.xValueFormatter?.(data[0]),
|
||||||
});
|
});
|
||||||
this.props.onContextMenu(
|
this.props.onContextMenu(
|
||||||
filters,
|
|
||||||
pointerEvent.clientX,
|
pointerEvent.clientX,
|
||||||
pointerEvent.clientY,
|
pointerEvent.clientY,
|
||||||
|
filters,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 controlPanel from './controlPanel';
|
||||||
import transformProps from './transformProps';
|
import transformProps from './transformProps';
|
||||||
import buildQuery from './buildQuery';
|
import buildQuery from './buildQuery';
|
||||||
|
@ -45,6 +45,7 @@ const metadata = new ChartMetadata({
|
||||||
t('Trend'),
|
t('Trend'),
|
||||||
],
|
],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
behaviors: [Behavior.DRILL_TO_DETAIL],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class BigNumberWithTrendlineChartPlugin extends ChartPlugin<
|
export default class BigNumberWithTrendlineChartPlugin extends ChartPlugin<
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsBoxPlot'),
|
loadChart: () => import('./EchartsBoxPlot'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Distribution'),
|
category: t('Distribution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default class EchartsFunnelChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsFunnel'),
|
loadChart: () => import('./EchartsFunnel'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('KPI'),
|
category: t('KPI'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default class EchartsGaugeChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsGauge'),
|
loadChart: () => import('./EchartsGauge'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('KPI'),
|
category: t('KPI'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { QueryObjectFilterClause } from '@superset-ui/core';
|
import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
|
||||||
import { EventHandlers } from '../types';
|
import { EventHandlers } from '../types';
|
||||||
import Echart from '../components/Echart';
|
import Echart from '../components/Echart';
|
||||||
import { GraphChartTransformedProps } from './types';
|
import { GraphChartTransformedProps } from './types';
|
||||||
|
@ -47,7 +47,7 @@ export default function EchartsGraph({
|
||||||
const sourceValue = data.find(item => item.id === e.data.source)?.name;
|
const sourceValue = data.find(item => item.id === e.data.source)?.name;
|
||||||
const targetValue = data.find(item => item.id === e.data.target)?.name;
|
const targetValue = data.find(item => item.id === e.data.target)?.name;
|
||||||
if (sourceValue && targetValue) {
|
if (sourceValue && targetValue) {
|
||||||
const filters: QueryObjectFilterClause[] = [
|
const filters: BinaryQueryObjectFilterClause[] = [
|
||||||
{
|
{
|
||||||
col: formData.source,
|
col: formData.source,
|
||||||
op: '==',
|
op: '==',
|
||||||
|
@ -61,7 +61,7 @@ export default function EchartsGraph({
|
||||||
formattedVal: targetValue,
|
formattedVal: targetValue,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 controlPanel from './controlPanel';
|
||||||
import transformProps from './transformProps';
|
import transformProps from './transformProps';
|
||||||
import thumbnail from './images/thumbnail.png';
|
import thumbnail from './images/thumbnail.png';
|
||||||
|
@ -46,6 +46,7 @@ export default class EchartsGraphChartPlugin extends ChartPlugin {
|
||||||
t('Transformable'),
|
t('Transformable'),
|
||||||
],
|
],
|
||||||
thumbnail,
|
thumbnail,
|
||||||
|
behaviors: [Behavior.DRILL_TO_DETAIL],
|
||||||
}),
|
}),
|
||||||
transformProps,
|
transformProps,
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import {
|
import {
|
||||||
PlainObject,
|
PlainObject,
|
||||||
QueryFormData,
|
QueryFormData,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
|
import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
|
||||||
import { SeriesTooltipOption } from 'echarts/types/src/util/types';
|
import { SeriesTooltipOption } from 'echarts/types/src/util/types';
|
||||||
|
@ -88,8 +88,8 @@ export type tooltipFormatParams = {
|
||||||
export type GraphChartTransformedProps = EchartsProps & {
|
export type GraphChartTransformedProps = EchartsProps & {
|
||||||
formData: PlainObject;
|
formData: PlainObject;
|
||||||
onContextMenu?: (
|
onContextMenu?: (
|
||||||
filters: QueryObjectFilterClause[],
|
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
AxisType,
|
AxisType,
|
||||||
DataRecordValue,
|
DataRecordValue,
|
||||||
DTTM_ALIAS,
|
DTTM_ALIAS,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { EchartsMixedTimeseriesChartTransformedProps } from './types';
|
import { EchartsMixedTimeseriesChartTransformedProps } from './types';
|
||||||
import Echart from '../components/Echart';
|
import Echart from '../components/Echart';
|
||||||
|
@ -128,7 +128,7 @@ export default function EchartsMixedTimeseries({
|
||||||
eventParams.seriesName
|
eventParams.seriesName
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (xAxis.type === AxisType.time) {
|
if (xAxis.type === AxisType.time) {
|
||||||
filters.push({
|
filters.push({
|
||||||
col:
|
col:
|
||||||
|
@ -154,7 +154,7 @@ export default function EchartsMixedTimeseries({
|
||||||
formattedVal: String(values[i]),
|
formattedVal: String(values[i]),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsMixedTimeseries'),
|
loadChart: () => import('./EchartsMixedTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default class EchartsPieChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsPie'),
|
loadChart: () => import('./EchartsPie'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Part of a Whole'),
|
category: t('Part of a Whole'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class EchartsRadarChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsRadar'),
|
loadChart: () => import('./EchartsRadar'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Ranking'),
|
category: t('Ranking'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default class EchartsAreaChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../EchartsTimeseries'),
|
loadChart: () => import('../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DTTM_ALIAS,
|
DTTM_ALIAS,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
AxisType,
|
AxisType,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { ViewRootGroup } from 'echarts/types/src/util/types';
|
import { ViewRootGroup } from 'echarts/types/src/util/types';
|
||||||
|
@ -191,7 +191,7 @@ export default function EchartsTimeseries({
|
||||||
...(eventParams.name ? [eventParams.name] : []),
|
...(eventParams.name ? [eventParams.name] : []),
|
||||||
...labelMap[eventParams.seriesName],
|
...labelMap[eventParams.seriesName],
|
||||||
];
|
];
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (xAxis.type === AxisType.time) {
|
if (xAxis.type === AxisType.time) {
|
||||||
filters.push({
|
filters.push({
|
||||||
col:
|
col:
|
||||||
|
@ -216,7 +216,7 @@ export default function EchartsTimeseries({
|
||||||
formattedVal: String(values[i]),
|
formattedVal: String(values[i]),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default class EchartsTimeseriesBarChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default class EchartsTimeseriesLineChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default class EchartsTimeseriesScatterChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default class EchartsTimeseriesSmoothLineChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../../EchartsTimeseries'),
|
loadChart: () => import('../../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class EchartsTimeseriesStepChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('../EchartsTimeseries'),
|
loadChart: () => import('../EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsTimeseries'),
|
loadChart: () => import('./EchartsTimeseries'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Evolution'),
|
category: t('Evolution'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: hasGenericChartAxes
|
description: hasGenericChartAxes
|
||||||
|
|
|
@ -16,7 +16,10 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { DataRecordValue, QueryObjectFilterClause } from '@superset-ui/core';
|
import {
|
||||||
|
DataRecordValue,
|
||||||
|
BinaryQueryObjectFilterClause,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import Echart from '../components/Echart';
|
import Echart from '../components/Echart';
|
||||||
import { NULL_STRING } from '../constants';
|
import { NULL_STRING } from '../constants';
|
||||||
|
@ -93,7 +96,7 @@ export default function EchartsTreemap({
|
||||||
const { treePath } = extractTreePathInfo(eventParams.treePathInfo);
|
const { treePath } = extractTreePathInfo(eventParams.treePathInfo);
|
||||||
if (treePath.length > 0) {
|
if (treePath.length > 0) {
|
||||||
const pointerEvent = eventParams.event.event;
|
const pointerEvent = eventParams.event.event;
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
treePath.forEach((path, i) =>
|
treePath.forEach((path, i) =>
|
||||||
filters.push({
|
filters.push({
|
||||||
col: groupby[i],
|
col: groupby[i],
|
||||||
|
@ -102,7 +105,7 @@ export default function EchartsTreemap({
|
||||||
formattedVal: path,
|
formattedVal: path,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default class EchartsTreemapChartPlugin extends ChartPlugin<
|
||||||
controlPanel,
|
controlPanel,
|
||||||
loadChart: () => import('./EchartsTreemap'),
|
loadChart: () => import('./EchartsTreemap'),
|
||||||
metadata: new ChartMetadata({
|
metadata: new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Part of a Whole'),
|
category: t('Part of a Whole'),
|
||||||
credits: ['https://echarts.apache.org'],
|
credits: ['https://echarts.apache.org'],
|
||||||
description: t(
|
description: t(
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import {
|
import {
|
||||||
HandlerFunction,
|
HandlerFunction,
|
||||||
QueryFormColumn,
|
QueryFormColumn,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
SetDataMaskHook,
|
SetDataMaskHook,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { EChartsCoreOption, ECharts } from 'echarts';
|
import { EChartsCoreOption, ECharts } from 'echarts';
|
||||||
|
@ -116,9 +116,9 @@ export interface EChartTransformedProps<F> {
|
||||||
selectedValues: Record<number, string>;
|
selectedValues: Record<number, string>;
|
||||||
legendData?: OptionName[];
|
legendData?: OptionName[];
|
||||||
onContextMenu?: (
|
onContextMenu?: (
|
||||||
filters: QueryObjectFilterClause[],
|
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { QueryObjectFilterClause } from '@superset-ui/core';
|
import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
|
||||||
import { EChartTransformedProps, EventHandlers } from '../types';
|
import { EChartTransformedProps, EventHandlers } from '../types';
|
||||||
|
|
||||||
export type Event = {
|
export type Event = {
|
||||||
|
@ -48,7 +48,7 @@ export const contextMenuEventHandler =
|
||||||
if (onContextMenu) {
|
if (onContextMenu) {
|
||||||
e.event.stop();
|
e.event.stop();
|
||||||
const pointerEvent = e.event.event;
|
const pointerEvent = e.event.event;
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (groupby.length > 0) {
|
if (groupby.length > 0) {
|
||||||
const values = labelMap[e.name];
|
const values = labelMap[e.name];
|
||||||
groupby.forEach((dimension, i) =>
|
groupby.forEach((dimension, i) =>
|
||||||
|
@ -60,7 +60,7 @@ export const contextMenuEventHandler =
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
onContextMenu(filters, pointerEvent.clientX, pointerEvent.clientY);
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import {
|
||||||
styled,
|
styled,
|
||||||
useTheme,
|
useTheme,
|
||||||
isAdhocColumn,
|
isAdhocColumn,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { PivotTable, sortAs, aggregatorTemplates } from './react-pivottable';
|
import { PivotTable, sortAs, aggregatorTemplates } from './react-pivottable';
|
||||||
import {
|
import {
|
||||||
|
@ -370,7 +370,8 @@ export default function PivotTableChart(props: PivotTableProps) {
|
||||||
) => {
|
) => {
|
||||||
if (onContextMenu) {
|
if (onContextMenu) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
e.stopPropagation();
|
||||||
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
if (colKey && colKey.length > 1) {
|
if (colKey && colKey.length > 1) {
|
||||||
colKey.forEach((val, i) => {
|
colKey.forEach((val, i) => {
|
||||||
const col = cols[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],
|
[cols, dateFormatters, onContextMenu, rows],
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default class PivotTableChartPlugin extends ChartPlugin<
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
const metadata = new ChartMetadata({
|
const metadata = new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Table'),
|
category: t('Table'),
|
||||||
description: t(
|
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.',
|
'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.',
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
NumberFormatter,
|
NumberFormatter,
|
||||||
QueryFormMetric,
|
QueryFormMetric,
|
||||||
QueryFormColumn,
|
QueryFormColumn,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { ColorFormatters } from '@superset-ui/chart-controls';
|
import { ColorFormatters } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
|
@ -74,9 +74,9 @@ interface PivotTableCustomizeProps {
|
||||||
legacy_order_by: QueryFormMetric[] | QueryFormMetric | null;
|
legacy_order_by: QueryFormMetric[] | QueryFormMetric | null;
|
||||||
order_desc: boolean;
|
order_desc: boolean;
|
||||||
onContextMenu?: (
|
onContextMenu?: (
|
||||||
filters: QueryObjectFilterClause[],
|
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -279,6 +279,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||||
onContextMenu={(e: MouseEvent) => {
|
onContextMenu={(e: MouseEvent) => {
|
||||||
if (onContextMenu) {
|
if (onContextMenu) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
onContextMenu(
|
onContextMenu(
|
||||||
row.original,
|
row.original,
|
||||||
e.nativeEvent.clientX,
|
e.nativeEvent.clientX,
|
||||||
|
|
|
@ -40,7 +40,7 @@ import {
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
getTimeFormatterForGranularity,
|
getTimeFormatterForGranularity,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
styled,
|
styled,
|
||||||
css,
|
css,
|
||||||
t,
|
t,
|
||||||
|
@ -630,7 +630,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||||
const handleContextMenu =
|
const handleContextMenu =
|
||||||
onContextMenu && !isRawRecords
|
onContextMenu && !isRawRecords
|
||||||
? (value: D, clientX: number, clientY: number) => {
|
? (value: D, clientX: number, clientY: number) => {
|
||||||
const filters: QueryObjectFilterClause[] = [];
|
const filters: BinaryQueryObjectFilterClause[] = [];
|
||||||
columnsMeta.forEach(col => {
|
columnsMeta.forEach(col => {
|
||||||
if (!col.isMetric) {
|
if (!col.isMetric) {
|
||||||
const dataRecordValue = value[col.key];
|
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;
|
: undefined;
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ export { default as __hack__ } from './types';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
const metadata = new ChartMetadata({
|
const metadata = new ChartMetadata({
|
||||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
|
||||||
category: t('Table'),
|
category: t('Table'),
|
||||||
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
|
||||||
description: t(
|
description: t(
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {
|
||||||
ChartDataResponseResult,
|
ChartDataResponseResult,
|
||||||
QueryFormData,
|
QueryFormData,
|
||||||
SetDataMaskHook,
|
SetDataMaskHook,
|
||||||
QueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { ColorFormatters, ColumnConfig } from '@superset-ui/chart-controls';
|
import { ColorFormatters, ColumnConfig } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
|
@ -113,9 +113,9 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
||||||
columnColorFormatters?: ColorFormatters;
|
columnColorFormatters?: ColorFormatters;
|
||||||
allowRearrangeColumns?: boolean;
|
allowRearrangeColumns?: boolean;
|
||||||
onContextMenu?: (
|
onContextMenu?: (
|
||||||
filters: QueryObjectFilterClause[],
|
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,13 +37,13 @@ export default {
|
||||||
viz_type: 'pie',
|
viz_type: 'pie',
|
||||||
slice_id: sliceId,
|
slice_id: sliceId,
|
||||||
slice_name: 'Genders',
|
slice_name: 'Genders',
|
||||||
granularity_sqla: null,
|
granularity_sqla: undefined,
|
||||||
time_grain_sqla: null,
|
time_grain_sqla: undefined,
|
||||||
since: '100 years ago',
|
since: '100 years ago',
|
||||||
until: 'now',
|
until: 'now',
|
||||||
metrics: ['sum__num'],
|
metrics: ['sum__num'],
|
||||||
groupby: ['gender'],
|
groupby: ['gender'],
|
||||||
limit: '25',
|
limit: 25,
|
||||||
pie_label_type: 'key',
|
pie_label_type: 'key',
|
||||||
donut: false,
|
donut: false,
|
||||||
show_legend: true,
|
show_legend: true,
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
|
||||||
import { URL_PARAMS } from 'src/constants';
|
import { URL_PARAMS } from 'src/constants';
|
||||||
import { getUrlParam } from 'src/utils/urlUtils';
|
import { getUrlParam } from 'src/utils/urlUtils';
|
||||||
import { isCurrentUserBot } from 'src/utils/isBot';
|
import { isCurrentUserBot } from 'src/utils/isBot';
|
||||||
|
import { ChartSource } from 'src/types/ChartSource';
|
||||||
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
||||||
import ChartRenderer from './ChartRenderer';
|
import ChartRenderer from './ChartRenderer';
|
||||||
import { ChartErrorMessage } from './ChartErrorMessage';
|
import { ChartErrorMessage } from './ChartErrorMessage';
|
||||||
|
@ -237,7 +238,7 @@ class Chart extends React.PureComponent {
|
||||||
subtitle={<MonospaceDiv>{message}</MonospaceDiv>}
|
subtitle={<MonospaceDiv>{message}</MonospaceDiv>}
|
||||||
copyText={message}
|
copyText={message}
|
||||||
link={queryResponse ? queryResponse.link : null}
|
link={queryResponse ? queryResponse.link : null}
|
||||||
source={dashboardId ? 'dashboard' : 'explore'}
|
source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
|
||||||
stackTrace={chartStackTrace}
|
stackTrace={chartStackTrace}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,82 +23,94 @@ import React, {
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} 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 { Menu } from 'src/components/Menu';
|
||||||
import { AntdDropdown as Dropdown } from 'src/components';
|
import { AntdDropdown as Dropdown } from 'src/components';
|
||||||
import ReactDOM from 'react-dom';
|
import { DrillDetailMenuItems } from './DrillDetail';
|
||||||
|
|
||||||
const MENU_ITEM_HEIGHT = 32;
|
const MENU_ITEM_HEIGHT = 32;
|
||||||
const MENU_VERTICAL_SPACING = 32;
|
const MENU_VERTICAL_SPACING = 32;
|
||||||
|
|
||||||
export interface ChartContextMenuProps {
|
export interface ChartContextMenuProps {
|
||||||
id: string;
|
id: number;
|
||||||
onSelection: (filters: QueryObjectFilterClause[]) => void;
|
formData: QueryFormData;
|
||||||
|
onSelection: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ref {
|
export interface Ref {
|
||||||
open: (
|
open: (
|
||||||
filters: QueryObjectFilterClause[],
|
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Filter = styled.span`
|
|
||||||
${({ theme }) => `
|
|
||||||
font-weight: ${theme.typography.weights.bold};
|
|
||||||
color: ${theme.colors.primary.base};
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ChartContextMenu = (
|
const ChartContextMenu = (
|
||||||
{ id, onSelection, onClose }: ChartContextMenuProps,
|
{ id, formData, onSelection, onClose }: ChartContextMenuProps,
|
||||||
ref: RefObject<Ref>,
|
ref: RefObject<Ref>,
|
||||||
) => {
|
) => {
|
||||||
const [state, setState] = useState<{
|
const canExplore = useSelector((state: RootState) =>
|
||||||
filters: QueryObjectFilterClause[];
|
findPermission('can_explore', 'Superset', state.user?.roles),
|
||||||
clientX: number;
|
|
||||||
clientY: number;
|
|
||||||
}>({ filters: [], 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 [{ filters, clientX, clientY }, setState] = useState<{
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
filters?: BinaryQueryObjectFilterClause[];
|
||||||
|
}>({ clientX: 0, clientY: 0 });
|
||||||
|
|
||||||
|
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(
|
const open = useCallback(
|
||||||
(filters: QueryObjectFilterClause[], clientX: number, clientY: number) => {
|
(
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
filters?: BinaryQueryObjectFilterClause[],
|
||||||
|
) => {
|
||||||
// Viewport height
|
// Viewport height
|
||||||
const vh = Math.max(
|
const vh = Math.max(
|
||||||
document.documentElement.clientHeight || 0,
|
document.documentElement.clientHeight || 0,
|
||||||
window.innerHeight || 0,
|
window.innerHeight || 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// +1 for automatically added options such as 'All' and 'Drill to detail'
|
const itemsCount =
|
||||||
const itemsCount = filters.length + 1;
|
[
|
||||||
|
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;
|
const menuHeight = MENU_ITEM_HEIGHT * itemsCount + MENU_VERTICAL_SPACING;
|
||||||
// Always show the context menu inside the viewport
|
// Always show the context menu inside the viewport
|
||||||
const adjustedY = vh - clientY < menuHeight ? vh - menuHeight : clientY;
|
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
|
// Since Ant Design's Dropdown does not offer an imperative API
|
||||||
// and we can't attach event triggers to charts SVG elements, we
|
// and we can't attach event triggers to charts SVG elements, we
|
||||||
|
@ -106,7 +118,7 @@ const ChartContextMenu = (
|
||||||
// from the charts.
|
// from the charts.
|
||||||
document.getElementById(`hidden-span-${id}`)?.click();
|
document.getElementById(`hidden-span-${id}`)?.click();
|
||||||
},
|
},
|
||||||
[id],
|
[id, showDrillToDetail],
|
||||||
);
|
);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
|
@ -119,7 +131,15 @@ const ChartContextMenu = (
|
||||||
|
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={menu}
|
overlay={
|
||||||
|
<Menu>
|
||||||
|
{menuItems.length ? (
|
||||||
|
menuItems
|
||||||
|
) : (
|
||||||
|
<Menu.Item disabled>No actions</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
onVisibleChange={value => !value && onClose()}
|
onVisibleChange={value => !value && onClose()}
|
||||||
>
|
>
|
||||||
|
@ -128,8 +148,8 @@ const ChartContextMenu = (
|
||||||
css={{
|
css={{
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: state.clientY,
|
top: clientY,
|
||||||
left: state.clientX,
|
left: clientX,
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -26,11 +26,12 @@ import {
|
||||||
t,
|
t,
|
||||||
isFeatureEnabled,
|
isFeatureEnabled,
|
||||||
FeatureFlag,
|
FeatureFlag,
|
||||||
|
getChartMetadataRegistry,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
|
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
|
||||||
import { EmptyStateBig, EmptyStateSmall } from 'src/components/EmptyState';
|
import { EmptyStateBig, EmptyStateSmall } from 'src/components/EmptyState';
|
||||||
|
import { ChartSource } from 'src/types/ChartSource';
|
||||||
import ChartContextMenu from './ChartContextMenu';
|
import ChartContextMenu from './ChartContextMenu';
|
||||||
import DrillDetailModal from './DrillDetailModal';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
annotationData: PropTypes.object,
|
annotationData: PropTypes.object,
|
||||||
|
@ -60,7 +61,7 @@ const propTypes = {
|
||||||
onFilterMenuClose: PropTypes.func,
|
onFilterMenuClose: PropTypes.func,
|
||||||
ownState: PropTypes.object,
|
ownState: PropTypes.object,
|
||||||
postTransformProps: PropTypes.func,
|
postTransformProps: PropTypes.func,
|
||||||
source: PropTypes.oneOf(['dashboard', 'explore']),
|
source: PropTypes.oneOf([ChartSource.Dashboard, ChartSource.Explore]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const BLANK = {};
|
const BLANK = {};
|
||||||
|
@ -83,8 +84,10 @@ class ChartRenderer extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
showContextMenu:
|
||||||
|
props.source === ChartSource.Dashboard &&
|
||||||
|
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL),
|
||||||
inContextMenu: false,
|
inContextMenu: false,
|
||||||
drillDetailFilters: null,
|
|
||||||
};
|
};
|
||||||
this.hasQueryResponseChange = false;
|
this.hasQueryResponseChange = false;
|
||||||
|
|
||||||
|
@ -97,14 +100,13 @@ class ChartRenderer extends React.Component {
|
||||||
this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
|
this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
|
||||||
this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
|
this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
|
||||||
this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
|
this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
|
||||||
|
this.onContextMenuFallback = this.onContextMenuFallback.bind(this);
|
||||||
const showContextMenu =
|
|
||||||
props.source === 'dashboard' &&
|
|
||||||
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL);
|
|
||||||
|
|
||||||
this.hooks = {
|
this.hooks = {
|
||||||
onAddFilter: this.handleAddFilter,
|
onAddFilter: this.handleAddFilter,
|
||||||
onContextMenu: showContextMenu ? this.handleOnContextMenu : undefined,
|
onContextMenu: this.state.showContextMenu
|
||||||
|
? this.handleOnContextMenu
|
||||||
|
: undefined,
|
||||||
onError: this.handleRenderFailure,
|
onError: this.handleRenderFailure,
|
||||||
setControlValue: this.handleSetControlValue,
|
setControlValue: this.handleSetControlValue,
|
||||||
onFilterMenuOpen: this.props.onFilterMenuOpen,
|
onFilterMenuOpen: this.props.onFilterMenuOpen,
|
||||||
|
@ -198,19 +200,28 @@ class ChartRenderer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOnContextMenu(filters, offsetX, offsetY) {
|
handleOnContextMenu(offsetX, offsetY, filters) {
|
||||||
this.contextMenuRef.current.open(filters, offsetX, offsetY);
|
this.contextMenuRef.current.open(offsetX, offsetY, filters);
|
||||||
this.setState({ inContextMenu: true });
|
this.setState({ inContextMenu: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenuSelected(filters) {
|
handleContextMenuSelected() {
|
||||||
this.setState({ inContextMenu: false, drillDetailFilters: filters });
|
this.setState({ inContextMenu: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleContextMenuClosed() {
|
handleContextMenuClosed() {
|
||||||
this.setState({ inContextMenu: false });
|
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() {
|
render() {
|
||||||
const { chartAlert, chartStatus, chartId } = this.props;
|
const { chartAlert, chartStatus, chartId } = this.props;
|
||||||
|
|
||||||
|
@ -265,7 +276,7 @@ class ChartRenderer extends React.Component {
|
||||||
let noResultsComponent;
|
let noResultsComponent;
|
||||||
const noResultTitle = t('No results were returned for this query');
|
const noResultTitle = t('No results were returned for this query');
|
||||||
const noResultDescription =
|
const noResultDescription =
|
||||||
this.props.source === 'explore'
|
this.props.source === ChartSource.Explore
|
||||||
? t(
|
? t(
|
||||||
'Make sure that the controls are configured properly and the datasource contains data for the selected time range',
|
'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 (
|
return (
|
||||||
<div>
|
<>
|
||||||
{this.props.source === 'dashboard' && (
|
{this.state.showContextMenu && (
|
||||||
<>
|
<ChartContextMenu
|
||||||
<ChartContextMenu
|
ref={this.contextMenuRef}
|
||||||
ref={this.contextMenuRef}
|
id={chartId}
|
||||||
id={chartId}
|
formData={currentFormData}
|
||||||
onSelection={this.handleContextMenuSelected}
|
onSelection={this.handleContextMenuSelected}
|
||||||
onClose={this.handleContextMenuClosed}
|
onClose={this.handleContextMenuClosed}
|
||||||
/>
|
/>
|
||||||
<DrillDetailModal
|
|
||||||
chartId={chartId}
|
|
||||||
initialFilters={this.state.drillDetailFilters}
|
|
||||||
formData={currentFormData}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<SuperChart
|
<div
|
||||||
disableErrorBoundary
|
onContextMenu={
|
||||||
key={`${chartId}${webpackHash}`}
|
this.state.showContextMenu ? this.onContextMenuFallback : undefined
|
||||||
id={`chart-id-${chartId}`}
|
}
|
||||||
className={chartClassName}
|
>
|
||||||
chartType={vizType}
|
<SuperChart
|
||||||
width={width}
|
disableErrorBoundary
|
||||||
height={height}
|
key={`${chartId}${webpackHash}`}
|
||||||
annotationData={annotationData}
|
id={`chart-id-${chartId}`}
|
||||||
datasource={datasource}
|
className={chartClassName}
|
||||||
initialValues={initialValues}
|
chartType={vizType}
|
||||||
formData={currentFormData}
|
width={width}
|
||||||
ownState={ownState}
|
height={height}
|
||||||
filterState={filterState}
|
annotationData={annotationData}
|
||||||
hooks={this.hooks}
|
datasource={datasource}
|
||||||
behaviors={behaviors}
|
initialValues={initialValues}
|
||||||
queriesData={queriesResponse}
|
formData={currentFormData}
|
||||||
onRenderSuccess={this.handleRenderSuccess}
|
ownState={ownState}
|
||||||
onRenderFailure={this.handleRenderFailure}
|
filterState={filterState}
|
||||||
noResults={noResultsComponent}
|
hooks={this.hooks}
|
||||||
postTransformProps={postTransformProps}
|
behaviors={behaviors}
|
||||||
inContextMenu={this.state.inContextMenu}
|
queriesData={queriesResponse}
|
||||||
/>
|
onRenderSuccess={this.handleRenderSuccess}
|
||||||
</div>
|
onRenderFailure={this.handleRenderFailure}
|
||||||
|
noResults={noResultsComponent}
|
||||||
|
postTransformProps={postTransformProps}
|
||||||
|
{...drillToDetailProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
|
@ -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;
|
|
@ -16,44 +16,15 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* 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 { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
|
||||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
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';
|
import DrillDetailModal from './DrillDetailModal';
|
||||||
|
|
||||||
const chart = chartQueries[sliceId];
|
jest.mock('./DrillDetailPane', () => () => null);
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockHistoryPush = jest.fn();
|
const mockHistoryPush = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
|
@ -62,32 +33,46 @@ jest.mock('react-router-dom', () => ({
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test('should render', async () => {
|
const { id: chartId, form_data: formData } = chartQueries[sliceId];
|
||||||
const { container } = await waitForRender();
|
const { slice_name: chartName } = formData;
|
||||||
expect(container).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)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<DrillDetailModalWrapper />, {
|
||||||
|
useRouter: true,
|
||||||
|
useRedux: true,
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
|
||||||
|
userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
|
||||||
|
await screen.findByRole('dialog', { name: `Drill to detail: ${chartName}` });
|
||||||
|
};
|
||||||
|
|
||||||
test('should render the title', async () => {
|
test('should render the title', async () => {
|
||||||
await waitForRender();
|
await renderModal();
|
||||||
expect(
|
expect(screen.getByText(`Drill to detail: ${chartName}`)).toBeInTheDocument();
|
||||||
screen.getByText(`Drill to detail: ${chart.form_data.slice_name}`),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render the modal', async () => {
|
|
||||||
await waitForRender();
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not render the modal', async () => {
|
|
||||||
await waitForRender({
|
|
||||||
initialFilters: undefined,
|
|
||||||
});
|
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render the button', async () => {
|
test('should render the button', async () => {
|
||||||
await waitForRender();
|
await renderModal();
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: 'Edit chart' }),
|
screen.getByRole('button', { name: 'Edit chart' }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
@ -95,14 +80,14 @@ test('should render the button', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close the modal', async () => {
|
test('should close the modal', async () => {
|
||||||
await waitForRender();
|
await renderModal();
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
userEvent.click(screen.getAllByRole('button', { name: 'Close' })[1]);
|
userEvent.click(screen.getAllByRole('button', { name: 'Close' })[1]);
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should forward to Explore', async () => {
|
test('should forward to Explore', async () => {
|
||||||
await waitForRender();
|
await renderModal();
|
||||||
userEvent.click(screen.getByRole('button', { name: 'Edit chart' }));
|
userEvent.click(screen.getByRole('button', { name: 'Edit chart' }));
|
||||||
expect(mockHistoryPush).toHaveBeenCalledWith(
|
expect(mockHistoryPush).toHaveBeenCalledWith(
|
||||||
`/explore/?dashboard_page_id=&slice_id=${sliceId}`,
|
`/explore/?dashboard_page_id=&slice_id=${sliceId}`,
|
|
@ -17,14 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, { useCallback, useContext, useMemo } from 'react';
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
BinaryQueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
|
@ -33,22 +26,51 @@ import {
|
||||||
t,
|
t,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from '@superset-ui/core';
|
} 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 { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
|
||||||
import { Slice } from 'src/types/Chart';
|
import { Slice } from 'src/types/Chart';
|
||||||
import Modal from '../Modal';
|
import DrillDetailPane from './DrillDetailPane';
|
||||||
import Button from '../Button';
|
|
||||||
|
|
||||||
const DrillDetailModal: React.FC<{
|
interface ModalFooterProps {
|
||||||
|
exploreChart: () => void;
|
||||||
|
closeModal?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
chartId: number;
|
||||||
initialFilters?: BinaryQueryObjectFilterClause[];
|
|
||||||
formData: QueryFormData;
|
formData: QueryFormData;
|
||||||
}> = ({ chartId, initialFilters, formData }) => {
|
initialFilters: BinaryQueryObjectFilterClause[];
|
||||||
const [showModal, setShowModal] = useState(false);
|
showModal: boolean;
|
||||||
const openModal = useCallback(() => setShowModal(true), []);
|
onHideModal: () => void;
|
||||||
const closeModal = useCallback(() => setShowModal(false), []);
|
}
|
||||||
const history = useHistory();
|
|
||||||
|
export default function DrillDetailModal({
|
||||||
|
chartId,
|
||||||
|
formData,
|
||||||
|
initialFilters,
|
||||||
|
showModal,
|
||||||
|
onHideModal,
|
||||||
|
}: DrillDetailModalProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const history = useHistory();
|
||||||
const dashboardPageId = useContext(DashboardPageIdContext);
|
const dashboardPageId = useContext(DashboardPageIdContext);
|
||||||
const { slice_name: chartName } = useSelector(
|
const { slice_name: chartName } = useSelector(
|
||||||
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
|
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
|
||||||
|
@ -64,43 +86,18 @@ const DrillDetailModal: React.FC<{
|
||||||
history.push(exploreUrl);
|
history.push(exploreUrl);
|
||||||
}, [exploreUrl, history]);
|
}, [exploreUrl, history]);
|
||||||
|
|
||||||
// Trigger modal open when initial filters change
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialFilters) {
|
|
||||||
openModal();
|
|
||||||
}
|
|
||||||
}, [initialFilters, openModal]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
show={showModal}
|
||||||
|
onHide={onHideModal ?? (() => null)}
|
||||||
css={css`
|
css={css`
|
||||||
.ant-modal-body {
|
.ant-modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
show={showModal}
|
|
||||||
onHide={closeModal}
|
|
||||||
title={t('Drill to detail: %s', chartName)}
|
title={t('Drill to detail: %s', chartName)}
|
||||||
footer={
|
footer={<ModalFooter exploreChart={exploreChart} />}
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
responsive
|
responsive
|
||||||
resizable
|
resizable
|
||||||
resizableConfig={{
|
resizableConfig={{
|
||||||
|
@ -117,6 +114,4 @@ const DrillDetailModal: React.FC<{
|
||||||
<DrillDetailPane formData={formData} initialFilters={initialFilters} />
|
<DrillDetailPane formData={formData} initialFilters={initialFilters} />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default DrillDetailModal;
|
|
|
@ -44,7 +44,7 @@ import MetadataBar, {
|
||||||
} from 'src/components/MetadataBar';
|
} from 'src/components/MetadataBar';
|
||||||
import Alert from 'src/components/Alert';
|
import Alert from 'src/components/Alert';
|
||||||
import { useApiV1Resource } from 'src/hooks/apiResources';
|
import { useApiV1Resource } from 'src/hooks/apiResources';
|
||||||
import TableControls from './TableControls';
|
import TableControls from './DrillDetailTableControls';
|
||||||
import { getDrillPayload } from './utils';
|
import { getDrillPayload } from './utils';
|
||||||
import { Dataset, ResultsPage } from './types';
|
import { Dataset, ResultsPage } from './types';
|
||||||
|
|
||||||
|
@ -55,12 +55,12 @@ export default function DrillDetailPane({
|
||||||
initialFilters,
|
initialFilters,
|
||||||
}: {
|
}: {
|
||||||
formData: QueryFormData;
|
formData: QueryFormData;
|
||||||
initialFilters?: BinaryQueryObjectFilterClause[];
|
initialFilters: BinaryQueryObjectFilterClause[];
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const lastPageIndex = useRef(pageIndex);
|
const lastPageIndex = useRef(pageIndex);
|
||||||
const [filters, setFilters] = useState(initialFilters || []);
|
const [filters, setFilters] = useState(initialFilters);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [responseError, setResponseError] = useState('');
|
const [responseError, setResponseError] = useState('');
|
||||||
const [resultsPages, setResultsPages] = useState<Map<number, ResultsPage>>(
|
const [resultsPages, setResultsPages] = useState<Map<number, ResultsPage>>(
|
||||||
|
@ -72,13 +72,13 @@ export default function DrillDetailPane({
|
||||||
state.common.conf.SAMPLES_ROW_LIMIT,
|
state.common.conf.SAMPLES_ROW_LIMIT,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extract datasource ID/type from string ID
|
// Extract datasource ID/type from string ID
|
||||||
const [datasourceId, datasourceType] = useMemo(
|
const [datasourceId, datasourceType] = useMemo(
|
||||||
() => formData.datasource.split('__'),
|
() => formData.datasource.split('__'),
|
||||||
[formData.datasource],
|
[formData.datasource],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get page of results
|
// Get page of results
|
||||||
const resultsPage = useMemo(() => {
|
const resultsPage = useMemo(() => {
|
||||||
const nextResultsPage = resultsPages.get(pageIndex);
|
const nextResultsPage = resultsPages.get(pageIndex);
|
||||||
if (nextResultsPage) {
|
if (nextResultsPage) {
|
||||||
|
@ -98,7 +98,7 @@ export default function DrillDetailPane({
|
||||||
formData.datasource,
|
formData.datasource,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Disable sorting on columns
|
// Disable sorting on columns
|
||||||
const sortDisabledColumns = useMemo(
|
const sortDisabledColumns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
columns.map(column => ({
|
columns.map(column => ({
|
||||||
|
@ -108,26 +108,26 @@ export default function DrillDetailPane({
|
||||||
[columns],
|
[columns],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update page index on pagination click
|
// Update page index on pagination click
|
||||||
const onServerPagination = useCallback(({ pageIndex }) => {
|
const onServerPagination = useCallback(({ pageIndex }) => {
|
||||||
setPageIndex(pageIndex);
|
setPageIndex(pageIndex);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Clear cache on reload button click
|
// Clear cache on reload button click
|
||||||
const handleReload = useCallback(() => {
|
const handleReload = useCallback(() => {
|
||||||
setResponseError('');
|
setResponseError('');
|
||||||
setResultsPages(new Map());
|
setResultsPages(new Map());
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Clear cache and reset page index if filters change
|
// Clear cache and reset page index if filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResponseError('');
|
setResponseError('');
|
||||||
setResultsPages(new Map());
|
setResultsPages(new Map());
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
// Update cache order if page in cache
|
// Update cache order if page in cache
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
resultsPages.has(pageIndex) &&
|
resultsPages.has(pageIndex) &&
|
||||||
|
@ -144,7 +144,7 @@ export default function DrillDetailPane({
|
||||||
}
|
}
|
||||||
}, [pageIndex, resultsPages]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!responseError && !isLoading && !resultsPages.has(pageIndex)) {
|
if (!responseError && !isLoading && !resultsPages.has(pageIndex)) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
@ -196,7 +196,7 @@ export default function DrillDetailPane({
|
||||||
|
|
||||||
let tableContent = null;
|
let tableContent = null;
|
||||||
if (responseError) {
|
if (responseError) {
|
||||||
// Render error if page download failed
|
// Render error if page download failed
|
||||||
tableContent = (
|
tableContent = (
|
||||||
<pre
|
<pre
|
||||||
css={css`
|
css={css`
|
||||||
|
@ -207,14 +207,14 @@ export default function DrillDetailPane({
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
} else if (!resultsPages.size) {
|
} else if (!resultsPages.size) {
|
||||||
// Render loading if first page hasn't loaded
|
// Render loading if first page hasn't loaded
|
||||||
tableContent = <Loading />;
|
tableContent = <Loading />;
|
||||||
} else if (resultsPage?.total === 0) {
|
} 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');
|
const title = t('No rows were returned for this dataset');
|
||||||
tableContent = <EmptyStateMedium image="document.svg" title={title} />;
|
tableContent = <EmptyStateMedium image="document.svg" title={title} />;
|
||||||
} else {
|
} else {
|
||||||
// Render table if at least one page has successfully loaded
|
// Render table if at least one page has successfully loaded
|
||||||
tableContent = (
|
tableContent = (
|
||||||
<TableView
|
<TableView
|
||||||
columns={sortDisabledColumns}
|
columns={sortDisabledColumns}
|
|
@ -19,7 +19,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import TableControls from './TableControls';
|
import TableControls from './DrillDetailTableControls';
|
||||||
|
|
||||||
const setFilters = jest.fn();
|
const setFilters = jest.fn();
|
||||||
const onReload = jest.fn();
|
const onReload = jest.fn();
|
|
@ -17,4 +17,4 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default } from './DrillDetailPane';
|
export { default as DrillDetailMenuItems } from './DrillDetailMenuItems';
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { getMockStore } from 'spec/fixtures/mockStore';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import { FeatureFlag } from 'src/featureFlags';
|
import { FeatureFlag } from 'src/featureFlags';
|
||||||
import SliceHeaderControls, { SliceHeaderControlsProps } from '.';
|
import SliceHeaderControls, { SliceHeaderControlsProps } from '.';
|
||||||
|
@ -96,9 +97,11 @@ const createProps = (viz_type = 'sunburst') =>
|
||||||
|
|
||||||
const renderWrapper = (overrideProps?: SliceHeaderControlsProps) => {
|
const renderWrapper = (overrideProps?: SliceHeaderControlsProps) => {
|
||||||
const props = overrideProps || createProps();
|
const props = overrideProps || createProps();
|
||||||
|
const store = getMockStore();
|
||||||
return render(<SliceHeaderControls {...props} />, {
|
return render(<SliceHeaderControls {...props} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
useRouter: true,
|
useRouter: true,
|
||||||
|
store,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -253,6 +256,7 @@ test('Should show the "Drill to detail"', () => {
|
||||||
[FeatureFlag.DRILL_TO_DETAIL]: true,
|
[FeatureFlag.DRILL_TO_DETAIL]: true,
|
||||||
};
|
};
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
|
props.slice.slice_id = 18;
|
||||||
renderWrapper(props);
|
renderWrapper(props);
|
||||||
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,7 +53,7 @@ import Button from 'src/components/Button';
|
||||||
import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal';
|
import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal';
|
||||||
import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane';
|
import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane';
|
||||||
import Modal from 'src/components/Modal';
|
import Modal from 'src/components/Modal';
|
||||||
import DrillDetailPane from 'src/dashboard/components/DrillDetailPane';
|
import { DrillDetailMenuItems } from 'src/components/Chart/DrillDetail';
|
||||||
|
|
||||||
const MENU_KEYS = {
|
const MENU_KEYS = {
|
||||||
CROSS_FILTER_SCOPING: 'cross_filter_scoping',
|
CROSS_FILTER_SCOPING: 'cross_filter_scoping',
|
||||||
|
@ -156,7 +156,7 @@ const dropdownIconsStyles = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DashboardChartModalTrigger = ({
|
const ViewResultsModalTrigger = ({
|
||||||
exploreUrl,
|
exploreUrl,
|
||||||
triggerNode,
|
triggerNode,
|
||||||
modalTitle,
|
modalTitle,
|
||||||
|
@ -205,7 +205,6 @@ const DashboardChartModalTrigger = ({
|
||||||
{t('Edit chart')}
|
{t('Edit chart')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-test="close-drilltodetail-modal"
|
|
||||||
buttonStyle="primary"
|
buttonStyle="primary"
|
||||||
buttonSize="small"
|
buttonSize="small"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
|
@ -430,7 +429,7 @@ class SliceHeaderControls extends React.PureComponent<
|
||||||
|
|
||||||
{this.props.supersetCanExplore && (
|
{this.props.supersetCanExplore && (
|
||||||
<Menu.Item key={MENU_KEYS.VIEW_RESULTS}>
|
<Menu.Item key={MENU_KEYS.VIEW_RESULTS}>
|
||||||
<DashboardChartModalTrigger
|
<ViewResultsModalTrigger
|
||||||
exploreUrl={this.props.exploreUrl}
|
exploreUrl={this.props.exploreUrl}
|
||||||
triggerNode={
|
triggerNode={
|
||||||
<span data-test="view-query-menu-item">
|
<span data-test="view-query-menu-item">
|
||||||
|
@ -453,18 +452,10 @@ class SliceHeaderControls extends React.PureComponent<
|
||||||
|
|
||||||
{isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) &&
|
{isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) &&
|
||||||
this.props.supersetCanExplore && (
|
this.props.supersetCanExplore && (
|
||||||
<Menu.Item key={MENU_KEYS.DRILL_TO_DETAIL}>
|
<DrillDetailMenuItems
|
||||||
<DashboardChartModalTrigger
|
chartId={slice.slice_id}
|
||||||
exploreUrl={this.props.exploreUrl}
|
formData={this.props.formData}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(slice.description || this.props.supersetCanExplore) && (
|
{(slice.description || this.props.supersetCanExplore) && (
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { chart } from 'src/components/Chart/chartReducer';
|
||||||
import componentTypes from 'src/dashboard/util/componentTypes';
|
import componentTypes from 'src/dashboard/util/componentTypes';
|
||||||
import { UrlParamEntries } from 'src/utils/urlUtils';
|
import { UrlParamEntries } from 'src/utils/urlUtils';
|
||||||
|
|
||||||
import { User } from 'src/types/bootstrapTypes';
|
import { BootstrapUser } from 'src/types/bootstrapTypes';
|
||||||
import { ChartState } from '../explore/types';
|
import { ChartState } from '../explore/types';
|
||||||
|
|
||||||
export { Dashboard } from 'src/types/Dashboard';
|
export { Dashboard } from 'src/types/Dashboard';
|
||||||
|
@ -117,7 +117,7 @@ export type RootState = {
|
||||||
dataMask: DataMaskStateWithId;
|
dataMask: DataMaskStateWithId;
|
||||||
impressionId: string;
|
impressionId: string;
|
||||||
nativeFilters: NativeFiltersState;
|
nativeFilters: NativeFiltersState;
|
||||||
user: User;
|
user: BootstrapUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** State of dashboardLayout in redux */
|
/** State of dashboardLayout in redux */
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum ChartSource {
|
||||||
|
Explore = 'explore',
|
||||||
|
Dashboard = 'dashboard',
|
||||||
|
}
|
Loading…
Reference in New Issue