Skip to content

Commit 6fb9288

Browse files
authored
feat: legend auto generation based on group by (#10529)
* feat: legend auto generation based on group by * chore: removed redundent check from util
1 parent 089a8ee commit 6fb9288

File tree

3 files changed

+107
-15
lines changed

3 files changed

+107
-15
lines changed

frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/QueryAddOns.tsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { MetricAggregation } from 'types/api/v5/queryRange';
1414
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
1515

1616
import HavingFilter from './HavingFilter/HavingFilter';
17+
import { buildDefaultLegendFromGroupBy } from './utils';
1718

1819
import './QueryAddOns.styles.scss';
1920

@@ -250,12 +251,33 @@ function QueryAddOns({
250251
}, [panelType, isListViewPanel, query, showReduceTo]);
251252

252253
const handleOptionClick = (e: RadioChangeEvent): void => {
253-
if (selectedViews.find((view) => view.key === e.target.value.key)) {
254-
setSelectedViews(
255-
selectedViews.filter((view) => view.key !== e.target.value.key),
254+
const clickedAddOn = e.target.value as AddOn;
255+
const isAlreadySelected = selectedViews.some(
256+
(view) => view.key === clickedAddOn.key,
257+
);
258+
259+
if (isAlreadySelected) {
260+
setSelectedViews((prev) =>
261+
prev.filter((view) => view.key !== clickedAddOn.key),
256262
);
257263
} else {
258-
setSelectedViews([...selectedViews, e.target.value]);
264+
// When enabling Legend format for the first time with an empty legend
265+
// and existing group-by keys, prefill the legend using all group-by keys.
266+
// This keeps existing custom legends intact and only helps seed a sensible default.
267+
if (
268+
clickedAddOn.key === ADD_ONS_KEYS.LEGEND_FORMAT &&
269+
isEmpty(query?.legend) &&
270+
Array.isArray(query.groupBy) &&
271+
query.groupBy.length > 0
272+
) {
273+
const defaultLegend = buildDefaultLegendFromGroupBy(query.groupBy);
274+
275+
if (defaultLegend) {
276+
handleChangeQueryLegend(defaultLegend);
277+
}
278+
}
279+
280+
setSelectedViews((prev) => [...prev, clickedAddOn]);
259281
}
260282
};
261283

@@ -288,12 +310,9 @@ function QueryAddOns({
288310
[handleSetQueryData, index, query],
289311
);
290312

291-
const handleRemoveView = useCallback(
292-
(key: string): void => {
293-
setSelectedViews(selectedViews.filter((view) => view.key !== key));
294-
},
295-
[selectedViews],
296-
);
313+
const handleRemoveView = useCallback((key: string): void => {
314+
setSelectedViews((prev) => prev.filter((view) => view.key !== key));
315+
}, []);
297316

298317
const handleChangeQueryLegend = useCallback(
299318
(value: string) => {
@@ -379,8 +398,8 @@ function QueryAddOns({
379398
<div className="input">
380399
<HavingFilter
381400
onClose={(): void => {
382-
setSelectedViews(
383-
selectedViews.filter((view) => view.key !== 'having'),
401+
setSelectedViews((prev) =>
402+
prev.filter((view) => view.key !== 'having'),
384403
);
385404
}}
386405
onChange={handleChangeHaving}
@@ -399,7 +418,9 @@ function QueryAddOns({
399418
initialValue={query?.limit ?? undefined}
400419
placeholder="Enter limit"
401420
onClose={(): void => {
402-
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
421+
setSelectedViews((prev) =>
422+
prev.filter((view) => view.key !== 'limit'),
423+
);
403424
}}
404425
closeIcon={<ChevronUp size={16} />}
405426
/>
@@ -482,8 +503,8 @@ function QueryAddOns({
482503
onChange={handleChangeQueryLegend}
483504
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
484505
onClose={(): void => {
485-
setSelectedViews(
486-
selectedViews.filter((view) => view.key !== 'legend_format'),
506+
setSelectedViews((prev) =>
507+
prev.filter((view) => view.key !== 'legend_format'),
487508
);
488509
}}
489510
closeIcon={<ChevronUp size={16} />}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
2+
3+
export const buildDefaultLegendFromGroupBy = (
4+
groupBy: IBuilderQuery['groupBy'],
5+
): string | null => {
6+
const segments = groupBy
7+
.map((item) => item?.key)
8+
.filter((key): key is string => Boolean(key))
9+
.map((key) => `${key} = {{${key}}}`);
10+
11+
if (segments.length === 0) {
12+
return null;
13+
}
14+
15+
return segments.join(', ');
16+
};

frontend/src/components/QueryBuilderV2/__tests__/QueryAddOns.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,59 @@ describe('QueryAddOns', () => {
275275
});
276276
});
277277
});
278+
279+
it('auto-generates legend from all groupBy keys when enabling Legend format with empty legend', async () => {
280+
const user = userEvent.setup();
281+
const query = baseQuery({
282+
groupBy: [{ key: 'service.name' }, { key: 'operation' }],
283+
});
284+
285+
render(
286+
<QueryAddOns
287+
query={query}
288+
version="v5"
289+
isListViewPanel={false}
290+
showReduceTo={false}
291+
panelType={PANEL_TYPES.TIME_SERIES}
292+
index={0}
293+
isForTraceOperator={false}
294+
/>,
295+
);
296+
297+
const legendTab = screen.getByTestId('query-add-on-legend_format');
298+
await user.click(legendTab);
299+
300+
expect(mockHandleChangeQueryData).toHaveBeenCalledWith(
301+
'legend',
302+
'service.name = {{service.name}}, operation = {{operation}}',
303+
);
304+
});
305+
306+
it('does not override existing legend when enabling Legend format', async () => {
307+
const user = userEvent.setup();
308+
const query = baseQuery({
309+
legend: 'existing legend',
310+
groupBy: [{ key: 'service.name' }],
311+
});
312+
313+
render(
314+
<QueryAddOns
315+
query={query}
316+
version="v5"
317+
isListViewPanel={false}
318+
showReduceTo={false}
319+
panelType={PANEL_TYPES.TIME_SERIES}
320+
index={0}
321+
isForTraceOperator={false}
322+
/>,
323+
);
324+
325+
const legendTab = screen.getByTestId('query-add-on-legend_format');
326+
await user.click(legendTab);
327+
328+
expect(mockHandleChangeQueryData).not.toHaveBeenCalledWith(
329+
'legend',
330+
expect.anything(),
331+
);
332+
});
278333
});

0 commit comments

Comments
 (0)