diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx index f931249082a..09d3e069942 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx @@ -3,7 +3,7 @@ import { render, screen, getAllByRole, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LokiQueryBuilder } from './LokiQueryBuilder'; import { LokiDatasource } from '../../datasource'; -import { LokiVisualQuery } from '../types'; +import { LokiOperationId, LokiVisualQuery } from '../types'; import { PanelData } from '@grafana/data'; const defaultQuery: LokiVisualQuery = { @@ -17,10 +17,20 @@ describe('LokiQueryBuilder', () => { datasource.languageProvider.fetchSeriesLabels = jest.fn().mockReturnValue({ job: ['a'], instance: ['b'] }); userEvent.click(screen.getByLabelText('Add')); const labels = screen.getByText(/Labels/); - const selects = getAllByRole(labels.parentElement!, 'combobox'); + const selects = getAllByRole(labels.parentElement!.parentElement!.parentElement!, 'combobox'); userEvent.click(selects[3]); await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument()); }); + + it('shows error for query with operations and no stream selector', async () => { + setup({ labels: [], operations: [{ id: LokiOperationId.Logfmt, params: [] }] }); + expect(screen.getByText('You need to specify at least 1 label filter (stream selector)')).toBeInTheDocument(); + }); + + it('shows no error for query with empty __line_contains operation and no stream selector', async () => { + setup({ labels: [], operations: [{ id: LokiOperationId.LineContains, params: [''] }] }); + expect(screen.queryByText('You need to specify at least 1 label filter (stream selector)')).not.toBeInTheDocument(); + }); }); function setup(query: LokiVisualQuery = defaultQuery, data?: PanelData) { diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 9b84ad88348..01f000e0e29 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { LokiVisualQuery } from '../types'; +import React, { useMemo } from 'react'; +import { LokiOperationId, LokiVisualQuery } from '../types'; import { LokiDatasource } from '../../datasource'; import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters'; import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList'; @@ -57,6 +57,18 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, return result[forLabelInterpolated] ?? []; }; + const labelFilterError: string | undefined = useMemo(() => { + const { labels, operations: op } = query; + if (!labels.length && op.length) { + // We don't want to show error for initial state with empty line contains operation + if (op.length === 1 && op[0].id === LokiOperationId.LineContains && op[0].params[0] === '') { + return undefined; + } + return 'You need to specify at least 1 label filter (stream selector)'; + } + return undefined; + }, [query]); + return ( <> @@ -69,6 +81,7 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, } labelsFilters={query.labels} onChange={onChangeLabels} + error={labelFilterError} /> diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx index bc0ca2608a6..87632d740f5 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx @@ -1,5 +1,6 @@ import { SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorList } from '@grafana/experimental'; +import { EditorFieldGroup, EditorList } from '@grafana/experimental'; +import { Field } from '@grafana/ui'; import { isEqual } from 'lodash'; import React, { useEffect, useState } from 'react'; import { QueryBuilderLabelFilter } from '../shared/types'; @@ -10,9 +11,10 @@ export interface Props { onChange: (labelFilters: QueryBuilderLabelFilter[]) => void; onGetLabelNames: (forLabel: Partial) => Promise; onGetLabelValues: (forLabel: Partial) => Promise; + error?: string; } -export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLabelValues }: Props) { +export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLabelValues, error }: Props) { const defaultOp = '='; const [items, setItems] = useState>>([{ op: defaultOp }]); @@ -36,7 +38,7 @@ export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLa return ( - + )} /> - + ); } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/testUtils.ts b/public/app/plugins/datasource/prometheus/querybuilder/testUtils.ts index d45952cbd65..8fe90238744 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/testUtils.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/testUtils.ts @@ -2,7 +2,7 @@ import { screen, getAllByRole } from '@testing-library/react'; export function getLabelSelects(index = 0) { const labels = screen.getByText(/Labels/); - const selects = getAllByRole(labels.parentElement!, 'combobox'); + const selects = getAllByRole(labels.parentElement!.parentElement!.parentElement!, 'combobox'); return { name: selects[3 * index], value: selects[3 * index + 2],