diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx index 5b4c490daa2..9c7e788ee15 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -1,6 +1,7 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor, findAllByRole } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { getSelectParent } from 'test/helpers/selectOptionInTest'; import { createLokiDatasource } from '../../mocks'; @@ -30,6 +31,96 @@ describe('LokiQueryBuilderContainer', () => { refId: 'A', }); }); + it('uses | to separate multiple values in label filters', async () => { + const props = { + query: { + expr: '{app="app1"}', + refId: 'A', + }, + datasource: createLokiDatasource(), + onChange: jest.fn(), + onRunQuery: () => {}, + showExplain: false, + }; + props.datasource.getDataSamples = jest.fn().mockResolvedValue([]); + props.datasource.languageProvider.fetchSeriesLabels = jest.fn().mockReturnValue({ job: ['grafana', 'loki'] }); + props.onChange = jest.fn(); + + render(); + await userEvent.click(screen.getByLabelText('Add')); + const labels = screen.getByText(/Label filters/); + const selects = await findAllByRole(getSelectParent(labels)!, 'combobox'); + await userEvent.click(selects[3]); + userEvent.click(await screen.findByText('job')); + + await userEvent.click(selects[4]); + userEvent.click(await screen.findByText('=~')); + + await userEvent.click(selects[5]); + userEvent.click(await screen.findByText('grafana')); + + await userEvent.click(selects[5]); + userEvent.click(await screen.findByText('loki')); + + await waitFor(() => { + expect(props.onChange).toBeCalledWith({ expr: '{app="app1", job=~"grafana|loki"}', refId: 'A' }); + }); + }); + + it('highlights the query in preview using loki grammar', async () => { + const props = { + query: { + expr: '{app="baz"} | logfmt', + refId: 'A', + }, + datasource: createLokiDatasource(), + onChange: jest.fn(), + onRunQuery: () => {}, + showExplain: false, + }; + props.datasource.getDataSamples = jest.fn().mockResolvedValue([]); + render(); + expect(screen.getByText('{')).toHaveClass('token punctuation'); + expect(screen.getByText('"baz"')).toHaveClass('token label-value attr-value'); + expect(screen.getByText('|')).toHaveClass('token pipe-operator operator'); + expect(screen.getByText('logfmt')).toHaveClass('token pipe-operations keyword'); + }); + + it('shows conflicting label expressions', async () => { + const props = { + query: { + expr: '{job="grafana"} | app!="bar" | app="bar"', + refId: 'A', + }, + datasource: createLokiDatasource(), + onChange: jest.fn(), + onRunQuery: () => {}, + showExplain: false, + }; + props.datasource.getDataSamples = jest.fn().mockResolvedValue([]); + + render(); + expect(screen.getAllByText('You have conflicting label filters')).toHaveLength(2); + }); + + it('uses as placeholder for query in explain section', async () => { + const props = { + query: { + expr: '{job="grafana"} | logfmt', + refId: 'A', + }, + datasource: createLokiDatasource(), + onChange: jest.fn(), + onRunQuery: () => {}, + showExplain: true, + }; + props.datasource.getDataSamples = jest.fn().mockResolvedValue([]); + + render(); + expect(screen.getByText('<')).toBeInTheDocument(); + expect(screen.getByText('expr')).toBeInTheDocument(); + expect(screen.getByText('>')).toBeInTheDocument(); + }); }); async function addOperation(section: string, op: string) { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx index 36c6f213c48..e8630e2cdb9 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import React, { useEffect, useId, useState } from 'react'; -import { Draggable } from 'react-beautiful-dnd'; +import { Draggable, DraggableProvided } from 'react-beautiful-dnd'; import { DataSourceApi, GrafanaTheme2, TimeRange } from '@grafana/data'; import { Button, Icon, InlineField, Tooltip, useTheme2, Stack } from '@grafana/ui'; @@ -145,6 +145,34 @@ export function OperationEditor({ return isConflicting ? true : undefined; }; + // We need to extract this into a component to prevent InlineField passing invalid to div which produces console error + const StyledOperationHeader = ({ provided }: { provided: DraggableProvided }) => ( +
+ +
{operationElements}
+ {restParam} + {index < query.operations.length - 1 && ( +
+
+
+
+ )} +
+ ); + return ( {(provided, snapshot) => ( @@ -153,34 +181,7 @@ export function OperationEditor({ invalid={isInvalid(snapshot.isDragging)} className={cx(styles.error, styles.cardWrapper)} > -
- -
{operationElements}
- {restParam} - {index < query.operations.length - 1 && ( -
-
-
-
- )} -
+ )}