fix: correctly set and unset the stackbarchart value across panel types (#9158)

This commit is contained in:
SagarRajput-7 2025-09-24 22:37:31 +05:30 committed by GitHub
parent c68096152d
commit 9114b44c0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 318 additions and 11 deletions

View File

@ -300,6 +300,7 @@ function RightContainer({
style={{ width: '100%' }} style={{ width: '100%' }}
className="panel-type-select" className="panel-type-select"
data-testid="panel-change-select" data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
> >
{graphTypes.map((item) => ( {graphTypes.map((item) => (
<Option key={item.name} value={item.name}> <Option key={item.name} value={item.name}>

View File

@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-duplicate-string */
// This test suite covers several important scenarios: // This test suite covers several important scenarios:
// - Empty layout - widget should be placed at origin (0,0) // - Empty layout - widget should be placed at origin (0,0)
// - Empty layout with custom dimensions // - Empty layout with custom dimensions
@ -6,13 +7,20 @@
// - Handling multiple rows correctly // - Handling multiple rows correctly
// - Handling widgets with different heights // - Handling widgets with different heights
import { screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider'; import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import i18n from 'ReactI18'; import i18n from 'ReactI18';
import { render } from 'tests/test-utils'; import {
fireEvent,
getByText as getByTextUtil,
render,
userEvent,
within,
} from 'tests/test-utils';
import NewWidget from '..'; import NewWidget from '..';
import { import {
@ -21,6 +29,28 @@ import {
placeWidgetBetweenRows, placeWidgetBetweenRows,
} from '../utils'; } from '../utils';
// Helper function to check stack series state
const checkStackSeriesState = (
container: HTMLElement,
expectedChecked: boolean,
): HTMLElement => {
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector(
'section > .stack-chart',
) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument();
const switchElement = within(stackSeriesSection).getByRole('switch');
if (expectedChecked) {
expect(switchElement).toBeChecked();
} else {
expect(switchElement).not.toBeChecked();
}
return switchElement;
};
const MOCK_SEARCH_PARAMS = const MOCK_SEARCH_PARAMS =
'?graphType=bar&widgetId=b473eef0-8eb5-4dd3-8089-c1817734084f&compositeQuery=%7B"id"%3A"f026c678-9abf-42af-a3dc-f73dc8cbb810"%2C"builder"%3A%7B"queryData"%3A%5B%7B"dataSource"%3A"metrics"%2C"queryName"%3A"A"%2C"aggregateOperator"%3A"count"%2C"aggregateAttribute"%3A%7B"id"%3A"----"%2C"dataType"%3A""%2C"key"%3A""%2C"type"%3A""%7D%2C"timeAggregation"%3A"rate"%2C"spaceAggregation"%3A"sum"%2C"filter"%3A%7B"expression"%3A""%7D%2C"aggregations"%3A%5B%7B"metricName"%3A""%2C"temporality"%3A""%2C"timeAggregation"%3A"count"%2C"spaceAggregation"%3A"sum"%2C"reduceTo"%3A"avg"%7D%5D%2C"functions"%3A%5B%5D%2C"filters"%3A%7B"items"%3A%5B%5D%2C"op"%3A"AND"%7D%2C"expression"%3A"A"%2C"disabled"%3Afalse%2C"stepInterval"%3Anull%2C"having"%3A%5B%5D%2C"limit"%3Anull%2C"orderBy"%3A%5B%5D%2C"groupBy"%3A%5B%5D%2C"legend"%3A""%2C"reduceTo"%3A"avg"%2C"source"%3A""%7D%5D%2C"queryFormulas"%3A%5B%5D%2C"queryTraceOperator"%3A%5B%5D%7D%2C"clickhouse_sql"%3A%5B%7B"name"%3A"A"%2C"legend"%3A""%2C"disabled"%3Afalse%2C"query"%3A""%7D%5D%2C"promql"%3A%5B%7B"name"%3A"A"%2C"query"%3A""%2C"legend"%3A""%2C"disabled"%3Afalse%7D%5D%2C"queryType"%3A"builder"%7D&relativeTime=30m'; '?graphType=bar&widgetId=b473eef0-8eb5-4dd3-8089-c1817734084f&compositeQuery=%7B"id"%3A"f026c678-9abf-42af-a3dc-f73dc8cbb810"%2C"builder"%3A%7B"queryData"%3A%5B%7B"dataSource"%3A"metrics"%2C"queryName"%3A"A"%2C"aggregateOperator"%3A"count"%2C"aggregateAttribute"%3A%7B"id"%3A"----"%2C"dataType"%3A""%2C"key"%3A""%2C"type"%3A""%7D%2C"timeAggregation"%3A"rate"%2C"spaceAggregation"%3A"sum"%2C"filter"%3A%7B"expression"%3A""%7D%2C"aggregations"%3A%5B%7B"metricName"%3A""%2C"temporality"%3A""%2C"timeAggregation"%3A"count"%2C"spaceAggregation"%3A"sum"%2C"reduceTo"%3A"avg"%7D%5D%2C"functions"%3A%5B%5D%2C"filters"%3A%7B"items"%3A%5B%5D%2C"op"%3A"AND"%7D%2C"expression"%3A"A"%2C"disabled"%3Afalse%2C"stepInterval"%3Anull%2C"having"%3A%5B%5D%2C"limit"%3Anull%2C"orderBy"%3A%5B%5D%2C"groupBy"%3A%5B%5D%2C"legend"%3A""%2C"reduceTo"%3A"avg"%2C"source"%3A""%7D%5D%2C"queryFormulas"%3A%5B%5D%2C"queryTraceOperator"%3A%5B%5D%7D%2C"clickhouse_sql"%3A%5B%7B"name"%3A"A"%2C"legend"%3A""%2C"disabled"%3Afalse%2C"query"%3A""%7D%5D%2C"promql"%3A%5B%7B"name"%3A"A"%2C"query"%3A""%2C"legend"%3A""%2C"disabled"%3Afalse%7D%5D%2C"queryType"%3A"builder"%7D&relativeTime=30m';
// Mocks // Mocks
@ -279,7 +309,7 @@ describe('Stacking bar in new panel', () => {
jest.fn(), jest.fn(),
]); ]);
const { container, getByText, getByRole } = render( const { container, getByText } = render(
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<DashboardProvider> <DashboardProvider>
<PreferenceContextProvider> <PreferenceContextProvider>
@ -305,7 +335,83 @@ describe('Stacking bar in new panel', () => {
expect(switchBtn).toBeInTheDocument(); expect(switchBtn).toBeInTheDocument();
expect(switchBtn).toHaveClass('ant-switch-checked'); expect(switchBtn).toHaveClass('ant-switch-checked');
// (Optional) More semantic: verify by role // Check that stack series is present and checked
expect(getByRole('switch')).toBeChecked(); checkStackSeriesState(container, true);
});
});
const STACKING_STATE_ATTR = 'data-stacking-state';
describe('when switching to BAR panel type', () => {
jest.setTimeout(10000);
beforeEach(() => {
jest.clearAllMocks();
// Mock useSearchParams to return the expected values
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams(MOCK_SEARCH_PARAMS),
jest.fn(),
]);
});
it('should preserve saved stacking value of true', async () => {
const { getByTestId, getByText, container } = render(
<DashboardProvider>
<NewWidget
selectedGraph={PANEL_TYPES.BAR}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</DashboardProvider>,
);
expect(getByTestId('panel-change-select')).toHaveAttribute(
STACKING_STATE_ATTR,
'true',
);
await userEvent.click(getByText('Bar')); // Panel Type Selected
// find dropdown with - .ant-select-dropdown
const panelDropdown = document.querySelector(
'.ant-select-dropdown',
) as HTMLElement;
expect(panelDropdown).toBeInTheDocument();
// Select TimeSeries from dropdown
const option = within(panelDropdown).getByText('Time Series');
fireEvent.click(option);
expect(getByTestId('panel-change-select')).toHaveAttribute(
STACKING_STATE_ATTR,
'false',
);
// Since we are on timeseries panel, stack series should be false
expect(screen.queryByText('Stack series')).not.toBeInTheDocument();
// switch back to Bar panel
const panelTypeDropdown2 = getByTestId('panel-change-select') as HTMLElement;
expect(panelTypeDropdown2).toBeInTheDocument();
expect(getByTextUtil(panelTypeDropdown2, 'Time Series')).toBeInTheDocument();
fireEvent.click(getByTextUtil(panelTypeDropdown2, 'Time Series'));
// find dropdown with - .ant-select-dropdown
const panelDropdown2 = document.querySelector(
'.ant-select-dropdown',
) as HTMLElement;
// // Select BAR from dropdown
const BarOption = within(panelDropdown2).getByText('Bar');
fireEvent.click(BarOption);
// Stack series should be true
checkStackSeriesState(container, true);
expect(getByTestId('panel-change-select')).toHaveAttribute(
STACKING_STATE_ATTR,
'true',
);
}); });
}); });

View File

@ -0,0 +1,134 @@
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export const BarNonStackedChartData = {
apiResponse: {
data: {
result: [
{
metric: {
'service.name': 'recommendationservice',
},
values: [
[1758713940, '33.933'],
[1758715020, '31.767'],
],
queryName: 'A',
metaData: {
alias: '__result_0',
index: 0,
queryName: 'A',
},
legend: '',
},
{
metric: {
'service.name': 'frontend',
},
values: [
[1758713940, '20.0'],
[1758715020, '25.0'],
],
queryName: 'B',
metaData: {
alias: '__result_1',
index: 1,
queryName: 'B',
},
legend: '',
},
],
resultType: 'time_series',
newResult: {
data: {
resultType: 'time_series',
result: [
{
queryName: 'A',
legend: '',
series: [
{
labels: {
'service.name': 'recommendationservice',
},
labelsArray: [
{
'service.name': 'recommendationservice',
},
],
values: [
{
timestamp: 1758713940000,
value: '33.933',
},
{
timestamp: 1758715020000,
value: '31.767',
},
],
metaData: {
alias: '__result_0',
index: 0,
queryName: 'A',
},
},
],
predictedSeries: [],
upperBoundSeries: [],
lowerBoundSeries: [],
anomalyScores: [],
list: null,
},
{
queryName: 'B',
legend: '',
series: [
{
labels: {
'service.name': 'frontend',
},
labelsArray: [
{
'service.name': 'frontend',
},
],
values: [
{
timestamp: 1758713940000,
value: '20.0',
},
{
timestamp: 1758715020000,
value: '25.0',
},
],
metaData: {
alias: '__result_1',
index: 1,
queryName: 'B',
},
},
],
predictedSeries: [],
upperBoundSeries: [],
lowerBoundSeries: [],
anomalyScores: [],
list: null,
},
],
},
},
},
} as MetricRangePayloadProps,
fillSpans: false,
stackedBarChart: false,
};
export const BarStackedChartData = {
...BarNonStackedChartData,
stackedBarChart: true,
};
export const TimeSeriesChartData = {
...BarNonStackedChartData,
stackedBarChart: false,
};

View File

@ -0,0 +1,50 @@
import { getUPlotChartData } from '../../../lib/uPlotLib/utils/getUplotChartData';
import {
BarNonStackedChartData,
BarStackedChartData,
TimeSeriesChartData,
} from './__mocks__/uplotChartData';
describe('getUplotChartData', () => {
it('should return the correct chart data for non-stacked bar chart', () => {
const result = getUPlotChartData(
BarNonStackedChartData.apiResponse,
BarNonStackedChartData.fillSpans,
BarNonStackedChartData.stackedBarChart,
);
expect(result).toEqual([
[1758713940, 1758715020],
[33.933, 31.767],
[20.0, 25.0],
]);
});
it('should return the correct chart data for stacked bar chart', () => {
const result = getUPlotChartData(
BarStackedChartData.apiResponse,
BarStackedChartData.fillSpans,
BarStackedChartData.stackedBarChart,
);
// For stacked charts, the values should be cumulative
// First series: [33.933, 31.767] + [20.0, 25.0] = [53.933, 56.767]
// Second series: [20.0, 25.0] (unchanged)
expect(result).toHaveLength(3);
expect(result[0]).toEqual([1758713940, 1758715020]);
expect(result[1][0]).toBeCloseTo(53.933, 3);
expect(result[1][1]).toBeCloseTo(56.767, 3);
expect(result[2]).toEqual([20.0, 25.0]);
});
it('should return the correct chart data for time series chart', () => {
const result = getUPlotChartData(
TimeSeriesChartData.apiResponse,
TimeSeriesChartData.fillSpans,
TimeSeriesChartData.stackedBarChart,
);
expect(result).toEqual([
[1758713940, 1758715020],
[33.933, 31.767],
[20.0, 25.0],
]);
});
});

View File

@ -595,6 +595,13 @@ function NewWidget({
selectedGraph, selectedGraph,
); );
setGraphType(type); setGraphType(type);
// with a single source of truth for stacking, we can use the saved stacking value as a default value
const savedStackingValue = getWidget()?.stackedBarChart;
setStackedBarChart(
type === PANEL_TYPES.BAR ? savedStackingValue || false : false,
);
redirectWithQueryBuilderData( redirectWithQueryBuilderData(
updatedQuery, updatedQuery,
{ [QueryParams.graphType]: type }, { [QueryParams.graphType]: type },

View File

@ -553,7 +553,7 @@ export const getDefaultWidgetData = (
timePreferance: 'GLOBAL_TIME', timePreferance: 'GLOBAL_TIME',
softMax: null, softMax: null,
softMin: null, softMin: null,
stackedBarChart: true, stackedBarChart: name === PANEL_TYPES.BAR,
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({ selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
...field, ...field,
type: field.fieldContext ?? '', type: field.fieldContext ?? '',

View File

@ -124,15 +124,23 @@ function UplotPanelWrapper({
queryResponse.data.payload.data.result = sortedSeriesData; queryResponse.data.payload.data.result = sortedSeriesData;
} }
const stackedBarChart = useMemo(
() =>
(selectedGraph
? selectedGraph === PANEL_TYPES.BAR
: widget?.panelTypes === PANEL_TYPES.BAR) && widget?.stackedBarChart,
[selectedGraph, widget?.panelTypes, widget?.stackedBarChart],
);
const chartData = getUPlotChartData( const chartData = getUPlotChartData(
queryResponse?.data?.payload, queryResponse?.data?.payload,
widget.fillSpans, widget.fillSpans,
widget?.stackedBarChart, stackedBarChart,
hiddenGraph, hiddenGraph,
); );
useEffect(() => { useEffect(() => {
if (widget.panelTypes === PANEL_TYPES.BAR && widget?.stackedBarChart) { if (widget.panelTypes === PANEL_TYPES.BAR && stackedBarChart) {
const graphV = cloneDeep(graphVisibility)?.slice(1); const graphV = cloneDeep(graphVisibility)?.slice(1);
const isSomeSelectedLegend = graphV?.some((v) => v === false); const isSomeSelectedLegend = graphV?.some((v) => v === false);
if (isSomeSelectedLegend) { if (isSomeSelectedLegend) {
@ -145,7 +153,7 @@ function UplotPanelWrapper({
} }
} }
} }
}, [graphVisibility, hiddenGraph, widget.panelTypes, widget?.stackedBarChart]); }, [graphVisibility, hiddenGraph, widget.panelTypes, stackedBarChart]);
const { timezone } = useTimezone(); const { timezone } = useTimezone();
@ -221,7 +229,7 @@ function UplotPanelWrapper({
setGraphsVisibilityStates: setGraphVisibility, setGraphsVisibilityStates: setGraphVisibility,
panelType: selectedGraph || widget.panelTypes, panelType: selectedGraph || widget.panelTypes,
currentQuery, currentQuery,
stackBarChart: widget?.stackedBarChart, stackBarChart: stackedBarChart,
hiddenGraph, hiddenGraph,
setHiddenGraph, setHiddenGraph,
customTooltipElement, customTooltipElement,
@ -261,6 +269,7 @@ function UplotPanelWrapper({
enableDrillDown, enableDrillDown,
onClickHandler, onClickHandler,
widget, widget,
stackedBarChart,
], ],
); );
@ -274,14 +283,14 @@ function UplotPanelWrapper({
items={menuItemsConfig.items} items={menuItemsConfig.items}
onClose={onClose} onClose={onClose}
/> />
{widget?.stackedBarChart && isFullViewMode && ( {stackedBarChart && isFullViewMode && (
<Alert <Alert
message="Selecting multiple legends is currently not supported in case of stacked bar charts" message="Selecting multiple legends is currently not supported in case of stacked bar charts"
type="info" type="info"
className="info-text" className="info-text"
/> />
)} )}
{isFullViewMode && setGraphVisibility && !widget?.stackedBarChart && ( {isFullViewMode && setGraphVisibility && !stackedBarChart && (
<GraphManager <GraphManager
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)} data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
name={widget.id} name={widget.id}