Loki: Remove unused query editors (#57192)

* Loki: Remove not used query editors

* Move Loki editor to components and rename

* Update public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx

Co-authored-by: Matias Chomicki <matyax@gmail.com>

* Fix test

Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
Ivana Huckova 2022-10-20 11:46:48 +02:00 committed by GitHub
parent cadc6088db
commit 3ee450e66b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 358 additions and 650 deletions

View File

@ -1,102 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { LoadingState, PanelData, toUtc, TimeRange, HistoryItem } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
import LokiLanguageProvider from '../LanguageProvider';
import { LokiDatasource } from '../datasource';
import { createLokiDatasource } from '../mocks';
import { LokiQuery } from '../types';
import { LokiExploreQueryEditor, Props } from './LokiExploreQueryEditor';
const setup = () => {
const mockTemplateSrv: TemplateSrv = {
getVariables: jest.fn(),
replace: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
const datasource: LokiDatasource = createLokiDatasource(mockTemplateSrv);
datasource.languageProvider = new LokiLanguageProvider(datasource);
jest.spyOn(datasource, 'metadataRequest').mockResolvedValue([]);
const onRunQuery = jest.fn();
const onChange = jest.fn();
const query: LokiQuery = { expr: '', refId: 'A', maxLines: 0 };
const range: TimeRange = {
from: toUtc('2020-01-01', 'YYYY-MM-DD'),
to: toUtc('2020-01-02', 'YYYY-MM-DD'),
raw: {
from: toUtc('2020-01-01', 'YYYY-MM-DD'),
to: toUtc('2020-01-02', 'YYYY-MM-DD'),
},
};
const data: PanelData = {
state: LoadingState.NotStarted,
series: [],
request: {
requestId: '1',
dashboardId: 1,
interval: '1s',
intervalMs: 1000,
panelId: 1,
range: {
from: toUtc('2020-01-01', 'YYYY-MM-DD'),
to: toUtc('2020-01-02', 'YYYY-MM-DD'),
raw: {
from: toUtc('2020-01-01', 'YYYY-MM-DD'),
to: toUtc('2020-01-02', 'YYYY-MM-DD'),
},
},
scopedVars: {},
targets: [],
timezone: 'GMT',
app: 'Grafana',
startTime: 0,
},
timeRange: {
from: toUtc('2020-01-01', 'YYYY-MM-DD'),
to: toUtc('2020-01-02', 'YYYY-MM-DD'),
raw: {
from: toUtc('2020-01-01', 'YYYY-MM-DD'),
to: toUtc('2020-01-02', 'YYYY-MM-DD'),
},
},
};
const history: Array<HistoryItem<LokiQuery>> = [];
const props: Props = {
query,
data,
range,
datasource,
history,
onChange,
onRunQuery,
};
render(<LokiExploreQueryEditor {...props} />);
};
describe('LokiExploreQueryEditor', () => {
let originalGetSelection: typeof window.getSelection;
beforeAll(() => {
originalGetSelection = window.getSelection;
window.getSelection = () => null;
});
afterAll(() => {
window.getSelection = originalGetSelection;
});
it('should render component without throwing an error', () => {
expect(() => setup()).not.toThrow();
});
it('should render LokiQueryField with ExtraFieldElement when ExploreMode is set to Logs', async () => {
setup();
expect(screen.getByLabelText('Loki extra field')).toBeInTheDocument();
});
});

View File

@ -1,46 +0,0 @@
// Libraries
import React, { memo } from 'react';
// Types
import { QueryEditorProps } from '@grafana/data';
import { LokiDatasource } from '../datasource';
import { LokiQuery, LokiOptions } from '../types';
import { LokiOptionFields } from './LokiOptionFields';
import { LokiQueryField } from './LokiQueryField';
export type Props = QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions>;
export const LokiExploreQueryEditor = memo((props: Props) => {
const { query, data, datasource, history, onChange, onRunQuery, range } = props;
return (
<LokiQueryField
datasource={datasource}
query={query}
onChange={onChange}
onBlur={() => {}}
onRunQuery={onRunQuery}
history={history}
data={data}
range={range}
data-testid={testIds.editor}
ExtraFieldElement={
<LokiOptionFields
lineLimitValue={query?.maxLines?.toString() || ''}
resolution={query.resolution || 1}
query={query}
onRunQuery={onRunQuery}
onChange={onChange}
/>
}
/>
);
});
LokiExploreQueryEditor.displayName = 'LokiExploreQueryEditor';
export const testIds = {
editor: 'loki-editor-explore',
};

View File

@ -1,72 +1,194 @@
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { cloneDeep, defaultsDeep } from 'lodash';
import React from 'react';
import { EventBusSrv, TimeRange, toUtc } from '@grafana/data';
import { setBackendSrv, TemplateSrv } from '@grafana/runtime';
import { BackendSrv } from 'app/core/services/backend_srv';
import { ContextSrv } from 'app/core/services/context_srv';
import { DataSourcePluginMeta } from '@grafana/data';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { createLokiDatasource } from '../mocks';
import { LokiQuery } from '../types';
import { LokiDatasource } from '../datasource';
import { EXPLAIN_LABEL_FILTER_CONTENT } from '../querybuilder/components/LokiQueryBuilderExplained';
import { LokiQuery, LokiQueryType } from '../types';
import { LokiQueryEditor } from './LokiQueryEditor';
import { LokiQueryEditorSelector } from './LokiQueryEditor';
const createMockRequestRange = (from: string, to: string): TimeRange => {
jest.mock('@grafana/runtime', () => {
return {
from: toUtc(from, 'YYYY-MM-DD'),
to: toUtc(to, 'YYYY-MM-DD'),
raw: {
from: toUtc(from, 'YYYY-MM-DD'),
to: toUtc(to, 'YYYY-MM-DD'),
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
};
});
jest.mock('app/core/store', () => {
return {
get() {
return undefined;
},
set() {},
getObject(key: string, defaultValue: unknown) {
return defaultValue;
},
};
});
const defaultQuery = {
refId: 'A',
expr: '{label1="foo", label2="bar"}',
};
const setup = (propOverrides?: object) => {
const mockTemplateSrv: TemplateSrv = {
getVariables: jest.fn(),
replace: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
const datasource = createLokiDatasource(mockTemplateSrv);
const onRunQuery = jest.fn();
const datasource = new LokiDatasource(
{
id: 1,
uid: '',
type: 'loki',
name: 'loki-test',
access: 'proxy',
url: '',
jsonData: {},
meta: {} as DataSourcePluginMeta,
readOnly: false,
},
undefined,
undefined
);
datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]);
datasource.getDataSamples = jest.fn().mockResolvedValue([]);
const defaultProps = {
datasource,
query: defaultQuery,
onRunQuery: () => {},
onChange: () => {},
};
describe('LokiQueryEditorSelector', () => {
it('shows code editor if expr and nothing else', async () => {
// We opt for showing code editor for queries created before this feature was added
render(<LokiQueryEditorSelector {...defaultProps} />);
expectCodeEditor();
});
it('shows builder if new query', async () => {
render(
<LokiQueryEditorSelector
{...defaultProps}
query={{
refId: 'A',
expr: '',
}}
/>
);
await expectBuilder();
});
it('shows code editor when code mode is set', async () => {
renderWithMode(QueryEditorMode.Code);
expectCodeEditor();
});
it('shows builder when builder mode is set', async () => {
renderWithMode(QueryEditorMode.Builder);
await expectBuilder();
});
it('changes to builder mode', async () => {
const { onChange } = renderWithMode(QueryEditorMode.Code);
await switchToMode(QueryEditorMode.Builder);
expect(onChange).toBeCalledWith({
refId: 'A',
expr: defaultQuery.expr,
queryType: LokiQueryType.Range,
editorMode: QueryEditorMode.Builder,
});
});
it('Can enable raw query', async () => {
renderWithMode(QueryEditorMode.Builder);
expect(await screen.findByLabelText('selector')).toBeInTheDocument();
screen.getByLabelText('Raw query').click();
expect(screen.queryByLabelText('selector')).not.toBeInTheDocument();
});
it('Should show raw query by default', async () => {
renderWithProps({
editorMode: QueryEditorMode.Builder,
expr: '{job="grafana"}',
});
const selector = await screen.findByLabelText('selector');
expect(selector).toBeInTheDocument();
expect(selector.textContent).toBe('{job="grafana"}');
});
it('Can enable explain', async () => {
renderWithMode(QueryEditorMode.Builder);
expect(screen.queryByText(EXPLAIN_LABEL_FILTER_CONTENT)).not.toBeInTheDocument();
screen.getByLabelText('Explain').click();
expect(await screen.findByText(EXPLAIN_LABEL_FILTER_CONTENT)).toBeInTheDocument();
});
it('changes to code mode', async () => {
const { onChange } = renderWithMode(QueryEditorMode.Builder);
await switchToMode(QueryEditorMode.Code);
expect(onChange).toBeCalledWith({
refId: 'A',
expr: defaultQuery.expr,
queryType: LokiQueryType.Range,
editorMode: QueryEditorMode.Code,
});
});
it('parses query when changing to builder mode', async () => {
const { rerender } = renderWithProps({
refId: 'A',
expr: 'rate({instance="host.docker.internal:3000"}[$__interval])',
editorMode: QueryEditorMode.Code,
});
await switchToMode(QueryEditorMode.Builder);
rerender(
<LokiQueryEditorSelector
{...defaultProps}
query={{
refId: 'A',
expr: 'rate({instance="host.docker.internal:3000"}[$__interval])',
editorMode: QueryEditorMode.Builder,
}}
/>
);
await screen.findByText('host.docker.internal:3000');
expect(screen.getByText('Rate')).toBeInTheDocument();
expect(screen.getByText('$__interval')).toBeInTheDocument();
});
});
function renderWithMode(mode: QueryEditorMode) {
return renderWithProps({ editorMode: mode });
}
function renderWithProps(overrides?: Partial<LokiQuery>) {
const query = defaultsDeep(overrides ?? {}, cloneDeep(defaultQuery));
const onChange = jest.fn();
const query: LokiQuery = {
expr: '',
refId: 'A',
legendFormat: 'My Legend',
};
const stuff = render(<LokiQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
return { onChange, ...stuff };
}
const range = createMockRequestRange('2020-01-01', '2020-01-02');
function expectCodeEditor() {
// Log browser shows this until log labels are loaded.
expect(screen.getByText('Loading labels...')).toBeInTheDocument();
}
const props = {
datasource,
onChange,
onRunQuery,
query,
range,
};
async function expectBuilder() {
expect(await screen.findByText('Label filters')).toBeInTheDocument();
}
Object.assign(props, propOverrides);
async function switchToMode(mode: QueryEditorMode) {
const label = {
[QueryEditorMode.Code]: /Code/,
[QueryEditorMode.Builder]: /Builder/,
}[mode];
render(<LokiQueryEditor {...props} />);
};
beforeAll(() => {
const mockedBackendSrv = new BackendSrv({
fromFetch: jest.fn(),
appEvents: new EventBusSrv(),
contextSrv: new ContextSrv(),
logout: jest.fn(),
});
setBackendSrv(mockedBackendSrv);
});
describe('LokiQueryEditor', () => {
it('should render without throwing', () => {
expect(() => setup()).not.toThrow();
});
});
const switchEl = screen.getByLabelText(label);
await userEvent.click(switchEl);
}

View File

@ -1,71 +1,165 @@
// Libraries
import React from 'react';
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
// Types
import { InlineFormLabel } from '@grafana/ui';
import { CoreApp, LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, EditorHeader, EditorRows, FlexItem, Space } from '@grafana/ui';
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import {
lokiQueryEditorExplainKey,
lokiQueryEditorRawQueryKey,
useFlag,
} from '../../prometheus/querybuilder/shared/hooks/useFlag';
import { LokiQueryBuilderContainer } from '../querybuilder/components/LokiQueryBuilderContainer';
import { LokiQueryBuilderOptions } from '../querybuilder/components/LokiQueryBuilderOptions';
import { LokiQueryCodeEditor } from '../querybuilder/components/LokiQueryCodeEditor';
import { QueryPatternsModal } from '../querybuilder/components/QueryPatternsModal';
import { buildVisualQueryFromString } from '../querybuilder/parsing';
import { changeEditorMode, getQueryWithDefaults } from '../querybuilder/state';
import { LokiQuery } from '../types';
import { LokiOptionFields } from './LokiOptionFields';
import { LokiQueryField } from './LokiQueryField';
import { LokiQueryEditorProps } from './types';
export function LokiQueryEditor(props: LokiQueryEditorProps) {
const { query, data, datasource, onChange, onRunQuery, range } = props;
const onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const nextQuery = { ...query, legendFormat: e.currentTarget.value };
onChange(nextQuery);
};
const legendField = (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
width={6}
tooltip="Controls the name of the time series, using name or pattern. For example
{{hostname}} will be replaced with label value for the label hostname. The legend only applies to metric queries."
>
Legend
</InlineFormLabel>
<input
type="text"
className="gf-form-input"
placeholder="legend format"
value={query.legendFormat || ''}
onChange={onLegendChange}
onBlur={onRunQuery}
/>
</div>
</div>
);
return (
<LokiQueryField
datasource={datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
onBlur={onRunQuery}
history={[]}
data={data}
data-testid={testIds.editor}
range={range}
ExtraFieldElement={
<>
<LokiOptionFields
lineLimitValue={query?.maxLines?.toString() || ''}
resolution={query?.resolution || 1}
query={query}
onRunQuery={onRunQuery}
onChange={onChange}
runOnBlur={true}
/>
{legendField}
</>
}
/>
);
}
export const testIds = {
editor: 'loki-editor',
};
export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props) => {
const { onChange, onRunQuery, onAddQuery, data, app, queries } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [queryPatternsModalOpen, setQueryPatternsModalOpen] = useState(false);
const [dataIsStale, setDataIsStale] = useState(false);
const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey);
const { flag: rawQuery, setFlag: setRawQuery } = useFlag(lokiQueryEditorRawQueryKey, true);
const query = getQueryWithDefaults(props.query);
// This should be filled in from the defaults by now.
const editorMode = query.editorMode!;
const onExplainChange = (event: SyntheticEvent<HTMLInputElement>) => {
setExplain(event.currentTarget.checked);
};
const onEditorModeChange = useCallback(
(newEditorMode: QueryEditorMode) => {
reportInteraction('grafana_loki_editor_mode_clicked', {
newEditor: newEditorMode,
previousEditor: query.editorMode ?? '',
newQuery: !query.expr,
app: app ?? '',
});
if (newEditorMode === QueryEditorMode.Builder) {
const result = buildVisualQueryFromString(query.expr || '');
// If there are errors, give user a chance to decide if they want to go to builder as that can lose some data.
if (result.errors.length) {
setParseModalOpen(true);
return;
}
}
changeEditorMode(query, newEditorMode, onChange);
},
[onChange, query, app]
);
useEffect(() => {
setDataIsStale(false);
}, [data]);
const onChangeInternal = (query: LokiQuery) => {
setDataIsStale(true);
onChange(query);
};
const onQueryPreviewChange = (event: SyntheticEvent<HTMLInputElement>) => {
const isEnabled = event.currentTarget.checked;
setRawQuery(isEnabled);
};
return (
<>
<ConfirmModal
isOpen={parseModalOpen}
title="Query parsing"
body="There were errors while trying to parse the query. Continuing to visual builder may lose some parts of the query."
confirmText="Continue"
onConfirm={() => {
onChange({ ...query, editorMode: QueryEditorMode.Builder });
setParseModalOpen(false);
}}
onDismiss={() => setParseModalOpen(false)}
/>
<QueryPatternsModal
isOpen={queryPatternsModalOpen}
onClose={() => setQueryPatternsModalOpen(false)}
query={query}
queries={queries}
app={app}
onChange={onChange}
onAddQuery={onAddQuery}
/>
<EditorHeader>
<Button
aria-label={selectors.components.QueryBuilder.queryPatterns}
variant="secondary"
size="sm"
onClick={() => {
setQueryPatternsModalOpen((prevValue) => !prevValue);
const visualQuery = buildVisualQueryFromString(query.expr || '');
reportInteraction('grafana_loki_query_patterns_opened', {
version: 'v2',
app: app ?? '',
editorMode: query.editorMode,
preSelectedOperationsCount: visualQuery.query.operations.length,
preSelectedLabelsCount: visualQuery.query.labels.length,
});
}}
>
Kick start your query
</Button>
<QueryHeaderSwitch label="Explain" value={explain} onChange={onExplainChange} />
{editorMode === QueryEditorMode.Builder && (
<>
<QueryHeaderSwitch label="Raw query" value={rawQuery} onChange={onQueryPreviewChange} />
</>
)}
<FlexItem grow={1} />
{app !== CoreApp.Explore && (
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run queries
</Button>
)}
<QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
</EditorHeader>
<Space v={0.5} />
<EditorRows>
{editorMode === QueryEditorMode.Code && (
<LokiQueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={explain} />
)}
{editorMode === QueryEditorMode.Builder && (
<LokiQueryBuilderContainer
datasource={props.datasource}
query={query}
onChange={onChangeInternal}
onRunQuery={props.onRunQuery}
showRawQuery={rawQuery}
showExplain={explain}
/>
)}
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} app={app} />
</EditorRows>
</>
);
});
LokiQueryEditorSelector.displayName = 'LokiQueryEditorSelector';

View File

@ -1,4 +1,4 @@
import { render, RenderResult } from '@testing-library/react';
import { render, RenderResult, waitFor } from '@testing-library/react';
import { noop } from 'lodash';
import React from 'react';
@ -6,7 +6,6 @@ import { CoreApp } from '@grafana/data';
import { LokiDatasource } from '../datasource';
import { testIds as exploreTestIds } from './LokiExploreQueryEditor';
import { testIds as regularTestIds } from './LokiQueryEditor';
import { LokiQueryEditorByApp } from './LokiQueryEditorByApp';
import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting';
@ -19,6 +18,8 @@ function setup(app: CoreApp): RenderResult {
getLabelKeys: () => [],
metrics: [],
},
getQueryHints: () => [],
getDataSamples: () => [],
} as unknown as LokiDatasource;
return render(
@ -40,24 +41,23 @@ describe('LokiQueryEditorByApp', () => {
expect(queryByTestId(regularTestIds.editor)).toBeNull();
});
it('should render regular query editor for unkown apps', () => {
it('should render regular query editor for unknown apps', async () => {
const { getByTestId, queryByTestId } = setup(CoreApp.Unknown);
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
expect(await waitFor(() => getByTestId(regularTestIds.editor))).toBeInTheDocument();
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
});
it('should render expore query editor for explore', () => {
it('should render regular query editor for explore', async () => {
const { getByTestId, queryByTestId } = setup(CoreApp.Explore);
expect(getByTestId(exploreTestIds.editor)).toBeInTheDocument();
expect(await waitFor(() => getByTestId(regularTestIds.editor))).toBeInTheDocument();
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
});
it('should render regular query editor for dashboard', () => {
const { getByTestId, queryByTestId } = setup(CoreApp.Dashboard);
it('should render regular query editor for dashboard', async () => {
const { findByTestId, queryByTestId } = setup(CoreApp.Dashboard);
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
expect(await findByTestId(regularTestIds.editor)).toBeInTheDocument();
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
});
});

View File

@ -1,12 +1,8 @@
import React, { memo } from 'react';
import { CoreApp } from '@grafana/data';
import { config } from '@grafana/runtime';
import { LokiQueryEditorSelector } from '../querybuilder/components/LokiQueryEditorSelector';
import { LokiExploreQueryEditor } from './LokiExploreQueryEditor';
import { LokiQueryEditor } from './LokiQueryEditor';
import { LokiQueryEditorSelector } from './LokiQueryEditor';
import { LokiQueryEditorForAlerting } from './LokiQueryEditorForAlerting';
import { LokiQueryEditorProps } from './types';
@ -16,17 +12,13 @@ export function LokiQueryEditorByApp(props: LokiQueryEditorProps) {
switch (app) {
case CoreApp.CloudAlerting:
return <LokiQueryEditorForAlerting {...props} />;
case CoreApp.Explore:
if (config.featureToggles.lokiQueryBuilder) {
return <LokiQueryEditorSelector {...props} />;
}
return <LokiExploreQueryEditor {...props} />;
default:
if (config.featureToggles.lokiQueryBuilder) {
return <LokiQueryEditorSelector {...props} />;
}
return <LokiQueryEditor {...props} />;
return <LokiQueryEditorSelector {...props} />;
}
}
export default memo(LokiQueryEditorByApp);
export const testIds = {
editor: 'loki-editor',
};

View File

@ -14,6 +14,7 @@ import {
QueryBuilderOperation,
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { testIds } from '../../components/LokiQueryEditor';
import { LokiDatasource } from '../../datasource';
import { escapeLabelValueInSelector } from '../../languageUtils';
import logqlGrammar from '../../syntax';
@ -107,7 +108,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
const lang = { grammar: logqlGrammar, name: 'logql' };
return (
<>
<div data-testid={testIds.editor}>
<EditorRow>
<LabelFilters
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
@ -170,7 +171,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
showExplain={showExplain}
/>
)}
</>
</div>
);
});

View File

@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import React, { useEffect, useReducer } from 'react';
import { testIds } from '../../components/LokiQueryEditor';
import { LokiDatasource } from '../../datasource';
import { LokiQuery } from '../../types';
import { lokiQueryModeller } from '../LokiQueryModeller';
@ -64,6 +65,7 @@ export function LokiQueryBuilderContainer(props: Props) {
onChange={onVisQueryChange}
onRunQuery={onRunQuery}
showExplain={showExplain}
data-testid={testIds.editor}
/>
{showRawQuery && <QueryPreview query={query.expr} />}
</>

View File

@ -37,8 +37,8 @@ export function LokiQueryCodeEditor({ query, datasource, range, onRunQuery, onCh
onBlur={onBlur}
history={[]}
data={data}
data-testid={testIds.editor}
app={app}
data-testid={testIds.editor}
/>
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
</div>

View File

@ -1,194 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { cloneDeep, defaultsDeep } from 'lodash';
import React from 'react';
import { DataSourcePluginMeta } from '@grafana/data';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { LokiDatasource } from '../../datasource';
import { LokiQuery, LokiQueryType } from '../../types';
import { EXPLAIN_LABEL_FILTER_CONTENT } from './LokiQueryBuilderExplained';
import { LokiQueryEditorSelector } from './LokiQueryEditorSelector';
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
reportInteraction: jest.fn(),
};
});
jest.mock('app/core/store', () => {
return {
get() {
return undefined;
},
set() {},
getObject(key: string, defaultValue: unknown) {
return defaultValue;
},
};
});
const defaultQuery = {
refId: 'A',
expr: '{label1="foo", label2="bar"}',
};
const datasource = new LokiDatasource(
{
id: 1,
uid: '',
type: 'loki',
name: 'loki-test',
access: 'proxy',
url: '',
jsonData: {},
meta: {} as DataSourcePluginMeta,
readOnly: false,
},
undefined,
undefined
);
datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]);
datasource.getDataSamples = jest.fn().mockResolvedValue([]);
const defaultProps = {
datasource,
query: defaultQuery,
onRunQuery: () => {},
onChange: () => {},
};
describe('LokiQueryEditorSelector', () => {
it('shows code editor if expr and nothing else', async () => {
// We opt for showing code editor for queries created before this feature was added
render(<LokiQueryEditorSelector {...defaultProps} />);
expectCodeEditor();
});
it('shows builder if new query', async () => {
render(
<LokiQueryEditorSelector
{...defaultProps}
query={{
refId: 'A',
expr: '',
}}
/>
);
await expectBuilder();
});
it('shows code editor when code mode is set', async () => {
renderWithMode(QueryEditorMode.Code);
expectCodeEditor();
});
it('shows builder when builder mode is set', async () => {
renderWithMode(QueryEditorMode.Builder);
await expectBuilder();
});
it('changes to builder mode', async () => {
const { onChange } = renderWithMode(QueryEditorMode.Code);
await switchToMode(QueryEditorMode.Builder);
expect(onChange).toBeCalledWith({
refId: 'A',
expr: defaultQuery.expr,
queryType: LokiQueryType.Range,
editorMode: QueryEditorMode.Builder,
});
});
it('Can enable raw query', async () => {
renderWithMode(QueryEditorMode.Builder);
expect(await screen.findByLabelText('selector')).toBeInTheDocument();
screen.getByLabelText('Raw query').click();
expect(screen.queryByLabelText('selector')).not.toBeInTheDocument();
});
it('Should show raw query by default', async () => {
renderWithProps({
editorMode: QueryEditorMode.Builder,
expr: '{job="grafana"}',
});
const selector = await screen.findByLabelText('selector');
expect(selector).toBeInTheDocument();
expect(selector.textContent).toBe('{job="grafana"}');
});
it('Can enable explain', async () => {
renderWithMode(QueryEditorMode.Builder);
expect(screen.queryByText(EXPLAIN_LABEL_FILTER_CONTENT)).not.toBeInTheDocument();
screen.getByLabelText('Explain').click();
expect(await screen.findByText(EXPLAIN_LABEL_FILTER_CONTENT)).toBeInTheDocument();
});
it('changes to code mode', async () => {
const { onChange } = renderWithMode(QueryEditorMode.Builder);
await switchToMode(QueryEditorMode.Code);
expect(onChange).toBeCalledWith({
refId: 'A',
expr: defaultQuery.expr,
queryType: LokiQueryType.Range,
editorMode: QueryEditorMode.Code,
});
});
it('parses query when changing to builder mode', async () => {
const { rerender } = renderWithProps({
refId: 'A',
expr: 'rate({instance="host.docker.internal:3000"}[$__interval])',
editorMode: QueryEditorMode.Code,
});
await switchToMode(QueryEditorMode.Builder);
rerender(
<LokiQueryEditorSelector
{...defaultProps}
query={{
refId: 'A',
expr: 'rate({instance="host.docker.internal:3000"}[$__interval])',
editorMode: QueryEditorMode.Builder,
}}
/>
);
await screen.findByText('host.docker.internal:3000');
expect(screen.getByText('Rate')).toBeInTheDocument();
expect(screen.getByText('$__interval')).toBeInTheDocument();
});
});
function renderWithMode(mode: QueryEditorMode) {
return renderWithProps({ editorMode: mode });
}
function renderWithProps(overrides?: Partial<LokiQuery>) {
const query = defaultsDeep(overrides ?? {}, cloneDeep(defaultQuery));
const onChange = jest.fn();
const stuff = render(<LokiQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
return { onChange, ...stuff };
}
function expectCodeEditor() {
// Log browser shows this until log labels are loaded.
expect(screen.getByText('Loading labels...')).toBeInTheDocument();
}
async function expectBuilder() {
expect(await screen.findByText('Label filters')).toBeInTheDocument();
}
async function switchToMode(mode: QueryEditorMode) {
const label = {
[QueryEditorMode.Code]: /Code/,
[QueryEditorMode.Builder]: /Builder/,
}[mode];
const switchEl = screen.getByLabelText(label);
await userEvent.click(switchEl);
}

View File

@ -1,161 +0,0 @@
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
import { CoreApp, LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, EditorHeader, EditorRows, FlexItem, Space } from '@grafana/ui';
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import {
lokiQueryEditorExplainKey,
lokiQueryEditorRawQueryKey,
useFlag,
} from '../../../prometheus/querybuilder/shared/hooks/useFlag';
import { LokiQueryEditorProps } from '../../components/types';
import { LokiQuery } from '../../types';
import { buildVisualQueryFromString } from '../parsing';
import { changeEditorMode, getQueryWithDefaults } from '../state';
import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer';
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
import { QueryPatternsModal } from './QueryPatternsModal';
export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props) => {
const { onChange, onRunQuery, onAddQuery, data, app, queries } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [queryPatternsModalOpen, setQueryPatternsModalOpen] = useState(false);
const [dataIsStale, setDataIsStale] = useState(false);
const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey);
const { flag: rawQuery, setFlag: setRawQuery } = useFlag(lokiQueryEditorRawQueryKey, true);
const query = getQueryWithDefaults(props.query);
// This should be filled in from the defaults by now.
const editorMode = query.editorMode!;
const onExplainChange = (event: SyntheticEvent<HTMLInputElement>) => {
setExplain(event.currentTarget.checked);
};
const onEditorModeChange = useCallback(
(newEditorMode: QueryEditorMode) => {
reportInteraction('grafana_loki_editor_mode_clicked', {
newEditor: newEditorMode,
previousEditor: query.editorMode ?? '',
newQuery: !query.expr,
app: app ?? '',
});
if (newEditorMode === QueryEditorMode.Builder) {
const result = buildVisualQueryFromString(query.expr || '');
// If there are errors, give user a chance to decide if they want to go to builder as that can lose some data.
if (result.errors.length) {
setParseModalOpen(true);
return;
}
}
changeEditorMode(query, newEditorMode, onChange);
},
[onChange, query, app]
);
useEffect(() => {
setDataIsStale(false);
}, [data]);
const onChangeInternal = (query: LokiQuery) => {
setDataIsStale(true);
onChange(query);
};
const onQueryPreviewChange = (event: SyntheticEvent<HTMLInputElement>) => {
const isEnabled = event.currentTarget.checked;
setRawQuery(isEnabled);
};
return (
<>
<ConfirmModal
isOpen={parseModalOpen}
title="Query parsing"
body="There were errors while trying to parse the query. Continuing to visual builder may lose some parts of the query."
confirmText="Continue"
onConfirm={() => {
onChange({ ...query, editorMode: QueryEditorMode.Builder });
setParseModalOpen(false);
}}
onDismiss={() => setParseModalOpen(false)}
/>
<QueryPatternsModal
isOpen={queryPatternsModalOpen}
onClose={() => setQueryPatternsModalOpen(false)}
query={query}
queries={queries}
app={app}
onChange={onChange}
onAddQuery={onAddQuery}
/>
<EditorHeader>
<Button
aria-label={selectors.components.QueryBuilder.queryPatterns}
variant="secondary"
size="sm"
onClick={() => {
setQueryPatternsModalOpen((prevValue) => !prevValue);
const visualQuery = buildVisualQueryFromString(query.expr || '');
reportInteraction('grafana_loki_query_patterns_opened', {
version: 'v2',
app: app ?? '',
editorMode: query.editorMode,
preSelectedOperationsCount: visualQuery.query.operations.length,
preSelectedLabelsCount: visualQuery.query.labels.length,
});
}}
>
Kick start your query
</Button>
<QueryHeaderSwitch label="Explain" value={explain} onChange={onExplainChange} />
{editorMode === QueryEditorMode.Builder && (
<>
<QueryHeaderSwitch label="Raw query" value={rawQuery} onChange={onQueryPreviewChange} />
</>
)}
<FlexItem grow={1} />
{app !== CoreApp.Explore && (
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run queries
</Button>
)}
<QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
</EditorHeader>
<Space v={0.5} />
<EditorRows>
{editorMode === QueryEditorMode.Code && (
<LokiQueryCodeEditor {...props} query={query} onChange={onChangeInternal} showExplain={explain} />
)}
{editorMode === QueryEditorMode.Builder && (
<LokiQueryBuilderContainer
datasource={props.datasource}
query={query}
onChange={onChangeInternal}
onRunQuery={props.onRunQuery}
showRawQuery={rawQuery}
showExplain={explain}
/>
)}
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} app={app} />
</EditorRows>
</>
);
});
LokiQueryEditorSelector.displayName = 'LokiQueryEditorSelector';