fix: Total calculation in stacked Timeseries charts (#24477)
This commit is contained in:
parent
51a34d7d58
commit
c5b4ecdca5
|
|
@ -30,7 +30,12 @@ import {
|
|||
FilterState,
|
||||
JsonObject,
|
||||
} from '../..';
|
||||
import { HandlerFunction, PlainObject, SetDataMaskHook } from '../types/Base';
|
||||
import {
|
||||
HandlerFunction,
|
||||
LegendState,
|
||||
PlainObject,
|
||||
SetDataMaskHook,
|
||||
} from '../types/Base';
|
||||
import { QueryData, DataRecordFilters } from '..';
|
||||
import { SupersetTheme } from '../../style';
|
||||
|
||||
|
|
@ -54,6 +59,8 @@ type Hooks = {
|
|||
onContextMenu?: HandlerFunction;
|
||||
/** handle errors */
|
||||
onError?: HandlerFunction;
|
||||
/** handle legend state changes */
|
||||
onLegendStateChanged?: HandlerFunction;
|
||||
/** use the vis as control to update state */
|
||||
setControlValue?: HandlerFunction;
|
||||
/** handle external filters */
|
||||
|
|
@ -88,6 +95,8 @@ export interface ChartPropsConfig {
|
|||
ownState?: JsonObject;
|
||||
/** Filter state that saved in dashboard */
|
||||
filterState?: FilterState;
|
||||
/** Legend state */
|
||||
legendState?: LegendState;
|
||||
/** Set of actual behaviors that this instance of chart should use */
|
||||
behaviors?: Behavior[];
|
||||
/** Chart display settings related to current view context */
|
||||
|
|
@ -128,6 +137,8 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
|
|||
|
||||
filterState: FilterState;
|
||||
|
||||
legendState?: LegendState;
|
||||
|
||||
queriesData: QueryData[];
|
||||
|
||||
width: number;
|
||||
|
|
@ -156,6 +167,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
|
|||
hooks = {},
|
||||
ownState = {},
|
||||
filterState = {},
|
||||
legendState,
|
||||
initialValues = {},
|
||||
queriesData = [],
|
||||
behaviors = [],
|
||||
|
|
@ -181,6 +193,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
|
|||
this.queriesData = queriesData;
|
||||
this.ownState = ownState;
|
||||
this.filterState = filterState;
|
||||
this.legendState = legendState;
|
||||
this.behaviors = behaviors;
|
||||
this.displaySettings = displaySettings;
|
||||
this.appSection = appSection;
|
||||
|
|
@ -205,6 +218,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
|
|||
input => input.width,
|
||||
input => input.ownState,
|
||||
input => input.filterState,
|
||||
input => input.legendState,
|
||||
input => input.behaviors,
|
||||
input => input.displaySettings,
|
||||
input => input.appSection,
|
||||
|
|
@ -224,6 +238,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
|
|||
width,
|
||||
ownState,
|
||||
filterState,
|
||||
legendState,
|
||||
behaviors,
|
||||
displaySettings,
|
||||
appSection,
|
||||
|
|
@ -243,6 +258,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
|
|||
queriesData,
|
||||
ownState,
|
||||
filterState,
|
||||
legendState,
|
||||
width,
|
||||
behaviors,
|
||||
displaySettings,
|
||||
|
|
|
|||
|
|
@ -100,4 +100,8 @@ export enum AxisType {
|
|||
log = 'log',
|
||||
}
|
||||
|
||||
export interface LegendState {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
import { EchartsMixedTimeseriesChartTransformedProps } from './types';
|
||||
import Echart from '../components/Echart';
|
||||
import { EventHandlers } from '../types';
|
||||
import { currentSeries, formatSeriesName } from '../utils/series';
|
||||
import { formatSeriesName } from '../utils/series';
|
||||
|
||||
export default function EchartsMixedTimeseries({
|
||||
height,
|
||||
|
|
@ -123,12 +123,6 @@ export default function EchartsMixedTimeseries({
|
|||
const { seriesName, seriesIndex } = props;
|
||||
handleChange(seriesName, seriesIndex);
|
||||
},
|
||||
mouseout: () => {
|
||||
currentSeries.name = '';
|
||||
},
|
||||
mouseover: params => {
|
||||
currentSeries.name = params.seriesName;
|
||||
},
|
||||
contextmenu: async eventParams => {
|
||||
if (onContextMenu) {
|
||||
eventParams.event.stop();
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ import {
|
|||
import { parseYAxisBound } from '../utils/controls';
|
||||
import {
|
||||
getOverMaxHiddenFormatter,
|
||||
currentSeries,
|
||||
dedupSeries,
|
||||
extractSeries,
|
||||
getAxisType,
|
||||
|
|
@ -481,11 +480,7 @@ export default function transformProps(
|
|||
seriesName: key,
|
||||
formatter: primarySeries.has(key) ? formatter : formatterSecondary,
|
||||
});
|
||||
if (currentSeries.name === key) {
|
||||
rows.push(`<span style="font-weight: 700">${content}</span>`);
|
||||
} else {
|
||||
rows.push(`<span style="opacity: 0.7">${content}</span>`);
|
||||
}
|
||||
rows.push(`<span style="opacity: 0.7">${content}</span>`);
|
||||
});
|
||||
return rows.join('<br />');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
getTimeFormatter,
|
||||
getColumnLabel,
|
||||
getNumberFormatter,
|
||||
LegendState,
|
||||
} from '@superset-ui/core';
|
||||
import { ViewRootGroup } from 'echarts/types/src/util/types';
|
||||
import GlobalModel from 'echarts/types/src/model/Global';
|
||||
|
|
@ -31,12 +32,11 @@ import ComponentModel from 'echarts/types/src/model/Component';
|
|||
import { EchartsHandler, EventHandlers } from '../types';
|
||||
import Echart from '../components/Echart';
|
||||
import { TimeseriesChartTransformedProps } from './types';
|
||||
import { currentSeries, formatSeriesName } from '../utils/series';
|
||||
import { formatSeriesName } from '../utils/series';
|
||||
import { ExtraControls } from '../components/ExtraControls';
|
||||
|
||||
const TIMER_DURATION = 300;
|
||||
|
||||
// @ts-ignore
|
||||
export default function EchartsTimeseries({
|
||||
formData,
|
||||
height,
|
||||
|
|
@ -49,6 +49,7 @@ export default function EchartsTimeseries({
|
|||
setControlValue,
|
||||
legendData = [],
|
||||
onContextMenu,
|
||||
onLegendStateChanged,
|
||||
xValueFormatter,
|
||||
xAxis,
|
||||
refs,
|
||||
|
|
@ -59,8 +60,6 @@ export default function EchartsTimeseries({
|
|||
const echartRef = useRef<EchartsHandler | null>(null);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
refs.echartRef = echartRef;
|
||||
const lastTimeRef = useRef(Date.now());
|
||||
const lastSelectedLegend = useRef('');
|
||||
const clickTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const extraControlRef = useRef<HTMLDivElement>(null);
|
||||
const [extraControlHeight, setExtraControlHeight] = useState(0);
|
||||
|
|
@ -69,34 +68,6 @@ export default function EchartsTimeseries({
|
|||
setExtraControlHeight(updatedHeight);
|
||||
}, [formData.showExtraControls]);
|
||||
|
||||
const handleDoubleClickChange = useCallback(
|
||||
(name?: string) => {
|
||||
const echartInstance = echartRef.current?.getEchartInstance();
|
||||
if (!name) {
|
||||
currentSeries.legend = '';
|
||||
echartInstance?.dispatchAction({
|
||||
type: 'legendAllSelect',
|
||||
});
|
||||
} else {
|
||||
legendData.forEach(datum => {
|
||||
if (datum === name) {
|
||||
currentSeries.legend = datum;
|
||||
echartInstance?.dispatchAction({
|
||||
type: 'legendSelect',
|
||||
name: datum,
|
||||
});
|
||||
} else {
|
||||
echartInstance?.dispatchAction({
|
||||
type: 'legendUnSelect',
|
||||
name: datum,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[legendData],
|
||||
);
|
||||
|
||||
const getModelInfo = (target: ViewRootGroup, globalModel: GlobalModel) => {
|
||||
let el = target;
|
||||
let model: ComponentModel | null = null;
|
||||
|
|
@ -175,30 +146,14 @@ export default function EchartsTimeseries({
|
|||
handleChange(name);
|
||||
}, TIMER_DURATION);
|
||||
},
|
||||
mouseout: () => {
|
||||
currentSeries.name = '';
|
||||
},
|
||||
mouseover: params => {
|
||||
currentSeries.name = params.seriesName;
|
||||
},
|
||||
legendselectchanged: payload => {
|
||||
const currentTime = Date.now();
|
||||
// TIMER_DURATION is the interval between two legendselectchanged event
|
||||
if (
|
||||
currentTime - lastTimeRef.current < TIMER_DURATION &&
|
||||
lastSelectedLegend.current === payload.name
|
||||
) {
|
||||
// execute dbclick
|
||||
handleDoubleClickChange(payload.name);
|
||||
} else {
|
||||
lastTimeRef.current = currentTime;
|
||||
// remember last selected legend
|
||||
lastSelectedLegend.current = payload.name;
|
||||
}
|
||||
// if all legend is unselected, we keep all selected
|
||||
if (Object.values(payload.selected).every(i => !i)) {
|
||||
handleDoubleClickChange();
|
||||
}
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
},
|
||||
legendselectall: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
},
|
||||
legendinverseselect: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
},
|
||||
contextmenu: async eventParams => {
|
||||
if (onContextMenu) {
|
||||
|
|
@ -272,15 +227,16 @@ export default function EchartsTimeseries({
|
|||
// @ts-ignore
|
||||
const globalModel = echartInstance.getModel();
|
||||
const model = getModelInfo(params.target, globalModel);
|
||||
const seriesCount = globalModel.getSeriesCount();
|
||||
const currentSeriesIndices = globalModel.getCurrentSeriesIndices();
|
||||
if (model) {
|
||||
const { name } = model;
|
||||
if (seriesCount !== currentSeriesIndices.length) {
|
||||
handleDoubleClickChange();
|
||||
} else {
|
||||
handleDoubleClickChange(name);
|
||||
}
|
||||
const legendState: LegendState = legendData.reduce(
|
||||
(previous, datum) => ({
|
||||
...previous,
|
||||
[datum]: datum === name,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
onLegendStateChanged?.(legendState);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -292,6 +248,7 @@ export default function EchartsTimeseries({
|
|||
<ExtraControls formData={formData} setControlValue={setControlValue} />
|
||||
</div>
|
||||
<Echart
|
||||
ref={echartRef}
|
||||
refs={refs}
|
||||
height={height - extraControlHeight}
|
||||
width={width}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import { ForecastSeriesEnum, ForecastValue, Refs } from '../types';
|
|||
import { parseYAxisBound } from '../utils/controls';
|
||||
import {
|
||||
calculateLowerLogTick,
|
||||
currentSeries,
|
||||
dedupSeries,
|
||||
extractDataTotalValues,
|
||||
extractSeries,
|
||||
|
|
@ -101,6 +100,7 @@ export default function transformProps(
|
|||
width,
|
||||
height,
|
||||
filterState,
|
||||
legendState,
|
||||
formData,
|
||||
hooks,
|
||||
queriesData,
|
||||
|
|
@ -192,6 +192,7 @@ export default function transformProps(
|
|||
stack,
|
||||
percentageThreshold,
|
||||
xAxisCol: xAxisLabel,
|
||||
legendState,
|
||||
},
|
||||
);
|
||||
const extraMetricLabels = extractExtraMetrics(chartProps.rawFormData).map(
|
||||
|
|
@ -221,6 +222,7 @@ export default function transformProps(
|
|||
stack,
|
||||
onlyTotal,
|
||||
isHorizontal,
|
||||
legendState,
|
||||
});
|
||||
const seriesContexts = extractForecastSeriesContexts(
|
||||
Object.values(rawSeries).map(series => series.name as string),
|
||||
|
|
@ -258,6 +260,7 @@ export default function transformProps(
|
|||
markerSize,
|
||||
areaOpacity: opacity,
|
||||
seriesType,
|
||||
legendState,
|
||||
stack,
|
||||
formatter,
|
||||
showValue,
|
||||
|
|
@ -379,6 +382,7 @@ export default function transformProps(
|
|||
setDataMask = () => {},
|
||||
setControlValue = () => {},
|
||||
onContextMenu,
|
||||
onLegendStateChanged,
|
||||
} = hooks;
|
||||
|
||||
const addYAxisLabelOffset = !!yAxisTitle;
|
||||
|
|
@ -486,7 +490,7 @@ export default function transformProps(
|
|||
seriesName: key,
|
||||
formatter,
|
||||
});
|
||||
if (currentSeries.name === key) {
|
||||
if (!legendState || legendState[key]) {
|
||||
rows.push(`<span style="font-weight: 700">${content}</span>`);
|
||||
} else {
|
||||
rows.push(`<span style="opacity: 0.7">${content}</span>`);
|
||||
|
|
@ -506,6 +510,7 @@ export default function transformProps(
|
|||
showLegend,
|
||||
theme,
|
||||
zoomable,
|
||||
legendState,
|
||||
),
|
||||
data: legendData as string[],
|
||||
},
|
||||
|
|
@ -549,6 +554,7 @@ export default function transformProps(
|
|||
width,
|
||||
legendData,
|
||||
onContextMenu,
|
||||
onLegendStateChanged,
|
||||
xValueFormatter: tooltipFormatter,
|
||||
xAxis: {
|
||||
label: xAxisLabel,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
getTimeFormatter,
|
||||
IntervalAnnotationLayer,
|
||||
isTimeseriesAnnotationResult,
|
||||
LegendState,
|
||||
NumberFormatter,
|
||||
smartDateDetailedFormatter,
|
||||
smartDateFormatter,
|
||||
|
|
@ -65,7 +66,7 @@ import {
|
|||
formatAnnotationLabel,
|
||||
parseAnnotationOpacity,
|
||||
} from '../utils/annotation';
|
||||
import { currentSeries, getChartPadding } from '../utils/series';
|
||||
import { getChartPadding } from '../utils/series';
|
||||
import {
|
||||
OpacityEnum,
|
||||
StackControlsValue,
|
||||
|
|
@ -156,6 +157,7 @@ export function transformSeries(
|
|||
yAxisIndex?: number;
|
||||
showValue?: boolean;
|
||||
onlyTotal?: boolean;
|
||||
legendState?: LegendState;
|
||||
formatter?: NumberFormatter;
|
||||
totalStackedValues?: number[];
|
||||
showValueIndexes?: number[];
|
||||
|
|
@ -182,6 +184,7 @@ export function transformSeries(
|
|||
showValue,
|
||||
onlyTotal,
|
||||
formatter,
|
||||
legendState,
|
||||
totalStackedValues = [],
|
||||
showValueIndexes = [],
|
||||
thresholdValues = [],
|
||||
|
|
@ -308,10 +311,14 @@ export function transformSeries(
|
|||
formatter: (params: any) => {
|
||||
const { value, dataIndex, seriesIndex, seriesName } = params;
|
||||
const numericValue = isHorizontal ? value[0] : value[1];
|
||||
const isSelectedLegend = currentSeries.legend === seriesName;
|
||||
const isSelectedLegend = !legendState || legendState[seriesName];
|
||||
const isAreaExpand = stack === StackControlsValue.Expand;
|
||||
if (!formatter) return numericValue;
|
||||
if (!stack || isSelectedLegend) return formatter(numericValue);
|
||||
if (!formatter) {
|
||||
return numericValue;
|
||||
}
|
||||
if (!stack && isSelectedLegend) {
|
||||
return formatter(numericValue);
|
||||
}
|
||||
if (!onlyTotal) {
|
||||
if (
|
||||
numericValue >=
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
ContextMenuFilters,
|
||||
FilterState,
|
||||
HandlerFunction,
|
||||
LegendState,
|
||||
PlainObject,
|
||||
QueryFormColumn,
|
||||
SetDataMaskHook,
|
||||
|
|
@ -127,6 +128,7 @@ export interface BaseTransformedProps<F> {
|
|||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
setDataMask?: SetDataMaskHook;
|
||||
onLegendStateChanged?: (state: LegendState) => void;
|
||||
filterState?: FilterState;
|
||||
refs: Refs;
|
||||
width: number;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
TimeFormatter,
|
||||
SupersetTheme,
|
||||
normalizeTimestamp,
|
||||
LegendState,
|
||||
} from '@superset-ui/core';
|
||||
import { SortSeriesType } from '@superset-ui/chart-controls';
|
||||
import { format, LegendComponentOption, SeriesOption } from 'echarts';
|
||||
|
|
@ -52,6 +53,7 @@ export function extractDataTotalValues(
|
|||
stack: StackType;
|
||||
percentageThreshold: number;
|
||||
xAxisCol: string;
|
||||
legendState?: LegendState;
|
||||
},
|
||||
): {
|
||||
totalStackedValues: number[];
|
||||
|
|
@ -59,13 +61,16 @@ export function extractDataTotalValues(
|
|||
} {
|
||||
const totalStackedValues: number[] = [];
|
||||
const thresholdValues: number[] = [];
|
||||
const { stack, percentageThreshold, xAxisCol } = opts;
|
||||
const { stack, percentageThreshold, xAxisCol, legendState } = opts;
|
||||
if (stack) {
|
||||
data.forEach(datum => {
|
||||
const values = Object.keys(datum).reduce((prev, curr) => {
|
||||
if (curr === xAxisCol) {
|
||||
return prev;
|
||||
}
|
||||
if (legendState && !legendState[curr]) {
|
||||
return prev;
|
||||
}
|
||||
const value = datum[curr] || 0;
|
||||
return prev + (value as number);
|
||||
}, 0);
|
||||
|
|
@ -85,23 +90,28 @@ export function extractShowValueIndexes(
|
|||
stack: StackType;
|
||||
onlyTotal?: boolean;
|
||||
isHorizontal?: boolean;
|
||||
legendState?: LegendState;
|
||||
},
|
||||
): number[] {
|
||||
const showValueIndexes: number[] = [];
|
||||
if (opts.stack) {
|
||||
const { legendState, stack, isHorizontal, onlyTotal } = opts;
|
||||
if (stack) {
|
||||
series.forEach((entry, seriesIndex) => {
|
||||
const { data = [] } = entry;
|
||||
(data as [any, number][]).forEach((datum, dataIndex) => {
|
||||
if (!opts.onlyTotal && datum[opts.isHorizontal ? 0 : 1] !== null) {
|
||||
if (entry.id && legendState && !legendState[entry.id]) {
|
||||
return;
|
||||
}
|
||||
if (!onlyTotal && datum[isHorizontal ? 0 : 1] !== null) {
|
||||
showValueIndexes[dataIndex] = seriesIndex;
|
||||
}
|
||||
if (opts.onlyTotal) {
|
||||
if (datum[opts.isHorizontal ? 0 : 1] > 0) {
|
||||
if (onlyTotal) {
|
||||
if (datum[isHorizontal ? 0 : 1] > 0) {
|
||||
showValueIndexes[dataIndex] = seriesIndex;
|
||||
}
|
||||
if (
|
||||
!showValueIndexes[dataIndex] &&
|
||||
datum[opts.isHorizontal ? 0 : 1] !== null
|
||||
datum[isHorizontal ? 0 : 1] !== null
|
||||
) {
|
||||
showValueIndexes[dataIndex] = seriesIndex;
|
||||
}
|
||||
|
|
@ -404,6 +414,7 @@ export function getLegendProps(
|
|||
show: boolean,
|
||||
theme: SupersetTheme,
|
||||
zoomable = false,
|
||||
legendState?: LegendState,
|
||||
): LegendComponentOption | LegendComponentOption[] {
|
||||
const legend: LegendComponentOption | LegendComponentOption[] = {
|
||||
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
|
||||
|
|
@ -413,6 +424,7 @@ export function getLegendProps(
|
|||
: 'vertical',
|
||||
show,
|
||||
type,
|
||||
selected: legendState,
|
||||
selector: ['all', 'inverse'],
|
||||
selectorLabel: {
|
||||
fontFamily: theme.typography.families.sansSerif,
|
||||
|
|
@ -495,12 +507,6 @@ export function sanitizeHtml(text: string): string {
|
|||
return format.encodeHTML(text);
|
||||
}
|
||||
|
||||
// TODO: Better use other method to maintain this state
|
||||
export const currentSeries = {
|
||||
name: '',
|
||||
legend: '',
|
||||
};
|
||||
|
||||
export function getAxisType(dataType?: GenericDataType): AxisType {
|
||||
if (dataType === GenericDataType.TEMPORAL) {
|
||||
return AxisType.time;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ class ChartRenderer extends React.Component {
|
|||
(isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) ||
|
||||
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)),
|
||||
inContextMenu: false,
|
||||
legendState: undefined,
|
||||
};
|
||||
this.hasQueryResponseChange = false;
|
||||
|
||||
|
|
@ -102,6 +103,7 @@ class ChartRenderer extends React.Component {
|
|||
this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
|
||||
this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
|
||||
this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
|
||||
this.handleLegendStateChanged = this.handleLegendStateChanged.bind(this);
|
||||
this.onContextMenuFallback = this.onContextMenuFallback.bind(this);
|
||||
|
||||
this.hooks = {
|
||||
|
|
@ -113,6 +115,7 @@ class ChartRenderer extends React.Component {
|
|||
setControlValue: this.handleSetControlValue,
|
||||
onFilterMenuOpen: this.props.onFilterMenuOpen,
|
||||
onFilterMenuClose: this.props.onFilterMenuClose,
|
||||
onLegendStateChanged: this.handleLegendStateChanged,
|
||||
setDataMask: dataMask => {
|
||||
this.props.actions?.updateDataMask(this.props.chartId, dataMask);
|
||||
},
|
||||
|
|
@ -226,6 +229,10 @@ class ChartRenderer extends React.Component {
|
|||
this.setState({ inContextMenu: false });
|
||||
}
|
||||
|
||||
handleLegendStateChanged(legendState) {
|
||||
this.setState({ legendState });
|
||||
}
|
||||
|
||||
// When viz plugins don't handle `contextmenu` event, fallback handler
|
||||
// calls `handleOnContextMenu` with no `filters` param.
|
||||
onContextMenuFallback(event) {
|
||||
|
|
@ -354,6 +361,7 @@ class ChartRenderer extends React.Component {
|
|||
noResults={noResultsComponent}
|
||||
postTransformProps={postTransformProps}
|
||||
emitCrossFilters={emitCrossFilters}
|
||||
legendState={this.state.legendState}
|
||||
{...drillToDetailProps}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue