From 6ec45f9d00a245d596655f46ed4332fc076f1a39 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:04:26 +0000 Subject: [PATCH] [release-12.3.1] Azure: Include aggregate columns in logs builder (#114835) Azure: Include aggregate columns in logs builder (#114684) * Include groupBy and aggregate columns * Order by tests (cherry picked from commit 94b7d6f7b8fd660ee30e05fd0e2cf980e5c81cd0) Co-authored-by: Andreas Christou --- .../LogsQueryBuilder/OrderBySection.test.tsx | 373 ++++++++++++++++++ .../LogsQueryBuilder/OrderBySection.tsx | 37 +- 2 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.test.tsx diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.test.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.test.tsx new file mode 100644 index 00000000000..471470aed86 --- /dev/null +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.test.tsx @@ -0,0 +1,373 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { + AzureQueryType, + BuilderQueryEditorExpressionType, + BuilderQueryEditorOrderByExpression, + BuilderQueryEditorOrderByOptions, + BuilderQueryEditorPropertyType, +} from '../../dataquery.gen'; +import { AzureMonitorQuery } from '../../types/query'; + +import { OrderBySection } from './OrderBySection'; + +describe('OrderBySection', () => { + const mockAllColumns = [ + { name: 'TimeGenerated', type: 'datetime' }, + { name: 'Level', type: 'string' }, + { name: 'Count', type: 'int' }, + { name: 'Duration', type: 'real' }, + ]; + + const createMockQuery = (orderBy?: BuilderQueryEditorOrderByExpression[]): AzureMonitorQuery => ({ + refId: 'A', + queryType: AzureQueryType.LogAnalytics, + azureLogAnalytics: { + builderQuery: { + from: { + type: BuilderQueryEditorExpressionType.Property, + property: { type: BuilderQueryEditorPropertyType.String, name: 'AppRequests' }, + }, + columns: { + type: BuilderQueryEditorExpressionType.Property, + columns: ['TimeGenerated', 'Level', 'Count'], + }, + orderBy: { + type: BuilderQueryEditorExpressionType.Order_by, + expressions: orderBy || [], + }, + reduce: { + type: BuilderQueryEditorExpressionType.Reduce, + expressions: [], + }, + groupBy: { + type: BuilderQueryEditorExpressionType.Group_by, + expressions: [], + }, + where: { + type: BuilderQueryEditorExpressionType.And, + expressions: [], + }, + }, + }, + }); + + const defaultProps = { + query: createMockQuery(), + allColumns: mockAllColumns, + buildAndUpdateQuery: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the order by section', () => { + render(); + expect(screen.getByText('Order By')).toBeInTheDocument(); + }); + + it('renders add button when no order by exists', () => { + render(); + const addButton = screen.getByLabelText('Add order by'); + expect(addButton).toBeInTheDocument(); + }); + + it('renders existing order by expressions', () => { + const existingOrderBy: BuilderQueryEditorOrderByExpression[] = [ + { + property: { name: 'TimeGenerated', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Desc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + { + property: { name: 'Level', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Asc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + ]; + + const queryWithOrderBy = createMockQuery(existingOrderBy); + render(); + + expect(screen.getAllByLabelText('Order by column')).toHaveLength(2); + expect(screen.getAllByLabelText('Order Direction')).toHaveLength(2); + expect(screen.getByText('TimeGenerated')).toBeInTheDocument(); + expect(screen.getByText('Level')).toBeInTheDocument(); + }); + + it('calls buildAndUpdateQuery when order by is added', async () => { + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + expect(defaultProps.buildAndUpdateQuery).toHaveBeenCalledWith({ + orderBy: expect.arrayContaining([ + expect.objectContaining({ + property: expect.objectContaining({ name: '' }), + order: BuilderQueryEditorOrderByOptions.Asc, + }), + ]), + }); + }); + + it('calls buildAndUpdateQuery when column is changed', async () => { + const existingOrderBy: BuilderQueryEditorOrderByExpression[] = [ + { + property: { name: 'TimeGenerated', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Desc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + ]; + + const queryWithOrderBy = createMockQuery(existingOrderBy); + render(); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + const levelOption = await screen.getByText('Level'); + await userEvent.click(levelOption); + + expect(defaultProps.buildAndUpdateQuery).toHaveBeenCalledWith({ + orderBy: expect.arrayContaining([ + expect.objectContaining({ + property: expect.objectContaining({ name: 'Level' }), + }), + ]), + }); + }); + + it('calls buildAndUpdateQuery when order direction is changed', async () => { + const existingOrderBy: BuilderQueryEditorOrderByExpression[] = [ + { + property: { name: 'TimeGenerated', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Asc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + ]; + + const queryWithOrderBy = createMockQuery(existingOrderBy); + render(); + + const orderSelect = screen.getByLabelText('Order Direction'); + await userEvent.click(orderSelect); + + const descOption = await screen.getByText('Descending'); + await userEvent.click(descOption); + + expect(defaultProps.buildAndUpdateQuery).toHaveBeenCalledWith({ + orderBy: expect.arrayContaining([ + expect.objectContaining({ + order: BuilderQueryEditorOrderByOptions.Desc, + }), + ]), + }); + }); + + it('calls buildAndUpdateQuery when order by is deleted', async () => { + const existingOrderBy: BuilderQueryEditorOrderByExpression[] = [ + { + property: { name: 'TimeGenerated', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Desc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + { + property: { name: 'Level', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Asc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + ]; + + const queryWithOrderBy = createMockQuery(existingOrderBy); + render(); + + const removeButtons = screen.getAllByLabelText('Remove order by'); + await userEvent.click(removeButtons[0]); + + expect(defaultProps.buildAndUpdateQuery).toHaveBeenCalledWith({ + orderBy: [ + expect.objectContaining({ + property: expect.objectContaining({ name: 'Level' }), + }), + ], + }); + }); + + it('uses group by columns when available', async () => { + const query = createMockQuery(); + query.azureLogAnalytics!.builderQuery!.groupBy = { + type: BuilderQueryEditorExpressionType.Group_by, + expressions: [ + { + property: { name: 'Level', type: BuilderQueryEditorPropertyType.String }, + type: BuilderQueryEditorExpressionType.Group_by, + }, + ], + }; + + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + expect(await screen.getByText('Level')).toBeInTheDocument(); + }); + + it('uses aggregate columns when available', async () => { + const query = createMockQuery(); + query.azureLogAnalytics!.builderQuery!.reduce = { + type: BuilderQueryEditorExpressionType.Reduce, + expressions: [ + { + reduce: { + name: 'sum', + type: BuilderQueryEditorPropertyType.Function, + }, + property: { name: 'Count', type: BuilderQueryEditorPropertyType.String }, + }, + ], + }; + + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + expect(await screen.getByText('Count')).toBeInTheDocument(); + }); + + it('uses both group by and aggregate columns when available', async () => { + const query = createMockQuery(); + query.azureLogAnalytics!.builderQuery!.groupBy = { + type: BuilderQueryEditorExpressionType.Group_by, + expressions: [ + { + property: { name: 'Level', type: BuilderQueryEditorPropertyType.String }, + type: BuilderQueryEditorExpressionType.Group_by, + }, + ], + }; + query.azureLogAnalytics!.builderQuery!.reduce = { + type: BuilderQueryEditorExpressionType.Reduce, + expressions: [ + { + reduce: { + name: 'sum', + type: BuilderQueryEditorPropertyType.Function, + }, + property: { name: 'Count', type: BuilderQueryEditorPropertyType.String }, + }, + ], + }; + + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + expect(await screen.getByText('Level')).toBeInTheDocument(); + expect(await screen.getByText('Count')).toBeInTheDocument(); + }); + + it('does not duplicate available columns', async () => { + const query = createMockQuery(); + query.azureLogAnalytics!.builderQuery!.groupBy = { + type: BuilderQueryEditorExpressionType.Group_by, + expressions: [ + { + property: { name: 'Level', type: BuilderQueryEditorPropertyType.String }, + type: BuilderQueryEditorExpressionType.Group_by, + }, + ], + }; + query.azureLogAnalytics!.builderQuery!.reduce = { + type: BuilderQueryEditorExpressionType.Reduce, + expressions: [ + { + reduce: { + name: 'sum', + type: BuilderQueryEditorPropertyType.Function, + }, + property: { name: 'Level', type: BuilderQueryEditorPropertyType.String }, + }, + ], + }; + + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + expect(await screen.getByText('Level')).toBeInTheDocument(); + }); + + it('uses selected columns when no group by or aggregates', async () => { + const query = createMockQuery(); + query.azureLogAnalytics!.builderQuery!.columns!.columns = ['TimeGenerated', 'Level']; + + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + expect(await screen.getByText('TimeGenerated')).toBeInTheDocument(); + expect(await screen.getByText('Level')).toBeInTheDocument(); + }); + + it('falls back to all columns when no other columns available', async () => { + const query = createMockQuery(); + query.azureLogAnalytics!.builderQuery!.columns!.columns = []; + + render(); + + const addButton = screen.getByLabelText('Add order by'); + await userEvent.click(addButton); + + const columnSelect = screen.getByLabelText('Order by column'); + await userEvent.click(columnSelect); + + expect(await screen.getByText('TimeGenerated')).toBeInTheDocument(); + expect(await screen.getByText('Level')).toBeInTheDocument(); + expect(await screen.getByText('Count')).toBeInTheDocument(); + expect(await screen.getByText('Duration')).toBeInTheDocument(); + }); + + it('resets order by when table changes', () => { + const existingOrderBy: BuilderQueryEditorOrderByExpression[] = [ + { + property: { name: 'TimeGenerated', type: BuilderQueryEditorPropertyType.String }, + order: BuilderQueryEditorOrderByOptions.Desc, + type: BuilderQueryEditorExpressionType.Order_by, + }, + ]; + + const queryWithOrderBy = createMockQuery(existingOrderBy); + const { rerender } = render(); + + const newQuery = createMockQuery(existingOrderBy); + newQuery.azureLogAnalytics!.builderQuery!.from!.property.name = 'AppEvents'; + + rerender(); + + const addButton = screen.getByLabelText('Add order by'); + expect(addButton).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx index 98587b1401f..ed8cde5ab39 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/OrderBySection.tsx @@ -45,16 +45,35 @@ export const OrderBySection: React.FC = ({ query, allColumn const aggregateColumns = builderQuery?.reduce?.expressions?.map((r) => r.property?.name) || []; const selectedColumns = builderQuery?.columns?.columns || []; - const allAvailableColumns = - groupByColumns.length > 0 - ? groupByColumns - : aggregateColumns.length > 0 - ? aggregateColumns - : selectedColumns.length > 0 - ? selectedColumns - : allColumns.map((col) => col.name); + const allAvailableColumns = new Set(); + if (groupByColumns.length > 0) { + groupByColumns.forEach((col) => { + if (col) { + allAvailableColumns.add(col); + } + }); + } + if (aggregateColumns.length > 0) { + aggregateColumns.forEach((col) => { + if (col) { + allAvailableColumns.add(col); + } + }); + } + if (allAvailableColumns.size === 0 && selectedColumns.length > 0) { + selectedColumns.forEach((col) => { + if (col) { + allAvailableColumns.add(col); + } + }); + } + if (allAvailableColumns.size === 0) { + allColumns.forEach((col) => { + allAvailableColumns.add(col.name); + }); + } - const columnOptions = allAvailableColumns.map((col) => ({ + const columnOptions = Array.from(allAvailableColumns).map((col) => ({ label: col, value: col, }));