mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
cadc6088db
commit
3ee450e66b
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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',
|
|
||||||
};
|
|
@ -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 React from 'react';
|
||||||
|
|
||||||
import { EventBusSrv, TimeRange, toUtc } from '@grafana/data';
|
import { DataSourcePluginMeta } from '@grafana/data';
|
||||||
import { setBackendSrv, TemplateSrv } from '@grafana/runtime';
|
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import { ContextSrv } from 'app/core/services/context_srv';
|
|
||||||
|
|
||||||
import { createLokiDatasource } from '../mocks';
|
import { LokiDatasource } from '../datasource';
|
||||||
import { LokiQuery } from '../types';
|
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 {
|
return {
|
||||||
from: toUtc(from, 'YYYY-MM-DD'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
to: toUtc(to, 'YYYY-MM-DD'),
|
reportInteraction: jest.fn(),
|
||||||
raw: {
|
};
|
||||||
from: toUtc(from, 'YYYY-MM-DD'),
|
});
|
||||||
to: toUtc(to, 'YYYY-MM-DD'),
|
|
||||||
|
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 datasource = new LokiDatasource(
|
||||||
const mockTemplateSrv: TemplateSrv = {
|
{
|
||||||
getVariables: jest.fn(),
|
id: 1,
|
||||||
replace: jest.fn(),
|
uid: '',
|
||||||
containsTemplate: jest.fn(),
|
type: 'loki',
|
||||||
updateTimeRange: jest.fn(),
|
name: 'loki-test',
|
||||||
};
|
access: 'proxy',
|
||||||
const datasource = createLokiDatasource(mockTemplateSrv);
|
url: '',
|
||||||
const onRunQuery = jest.fn();
|
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 onChange = jest.fn();
|
||||||
|
|
||||||
const query: LokiQuery = {
|
const stuff = render(<LokiQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
|
||||||
expr: '',
|
return { onChange, ...stuff };
|
||||||
refId: 'A',
|
}
|
||||||
legendFormat: 'My Legend',
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = {
|
async function expectBuilder() {
|
||||||
datasource,
|
expect(await screen.findByText('Label filters')).toBeInTheDocument();
|
||||||
onChange,
|
}
|
||||||
onRunQuery,
|
|
||||||
query,
|
|
||||||
range,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
async function switchToMode(mode: QueryEditorMode) {
|
||||||
|
const label = {
|
||||||
|
[QueryEditorMode.Code]: /Code/,
|
||||||
|
[QueryEditorMode.Builder]: /Builder/,
|
||||||
|
}[mode];
|
||||||
|
|
||||||
render(<LokiQueryEditor {...props} />);
|
const switchEl = screen.getByLabelText(label);
|
||||||
};
|
await userEvent.click(switchEl);
|
||||||
|
}
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -1,71 +1,165 @@
|
|||||||
// Libraries
|
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// Types
|
import { CoreApp, LoadingState } from '@grafana/data';
|
||||||
import { InlineFormLabel } from '@grafana/ui';
|
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';
|
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 = {
|
export const testIds = {
|
||||||
editor: 'loki-editor',
|
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';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { render, RenderResult } from '@testing-library/react';
|
import { render, RenderResult, waitFor } from '@testing-library/react';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@ -6,7 +6,6 @@ import { CoreApp } from '@grafana/data';
|
|||||||
|
|
||||||
import { LokiDatasource } from '../datasource';
|
import { LokiDatasource } from '../datasource';
|
||||||
|
|
||||||
import { testIds as exploreTestIds } from './LokiExploreQueryEditor';
|
|
||||||
import { testIds as regularTestIds } from './LokiQueryEditor';
|
import { testIds as regularTestIds } from './LokiQueryEditor';
|
||||||
import { LokiQueryEditorByApp } from './LokiQueryEditorByApp';
|
import { LokiQueryEditorByApp } from './LokiQueryEditorByApp';
|
||||||
import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting';
|
import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting';
|
||||||
@ -19,6 +18,8 @@ function setup(app: CoreApp): RenderResult {
|
|||||||
getLabelKeys: () => [],
|
getLabelKeys: () => [],
|
||||||
metrics: [],
|
metrics: [],
|
||||||
},
|
},
|
||||||
|
getQueryHints: () => [],
|
||||||
|
getDataSamples: () => [],
|
||||||
} as unknown as LokiDatasource;
|
} as unknown as LokiDatasource;
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@ -40,24 +41,23 @@ describe('LokiQueryEditorByApp', () => {
|
|||||||
expect(queryByTestId(regularTestIds.editor)).toBeNull();
|
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);
|
const { getByTestId, queryByTestId } = setup(CoreApp.Unknown);
|
||||||
|
expect(await waitFor(() => getByTestId(regularTestIds.editor))).toBeInTheDocument();
|
||||||
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
|
||||||
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
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);
|
const { getByTestId, queryByTestId } = setup(CoreApp.Explore);
|
||||||
|
|
||||||
expect(getByTestId(exploreTestIds.editor)).toBeInTheDocument();
|
expect(await waitFor(() => getByTestId(regularTestIds.editor))).toBeInTheDocument();
|
||||||
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render regular query editor for dashboard', () => {
|
it('should render regular query editor for dashboard', async () => {
|
||||||
const { getByTestId, queryByTestId } = setup(CoreApp.Dashboard);
|
const { findByTestId, queryByTestId } = setup(CoreApp.Dashboard);
|
||||||
|
|
||||||
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
expect(await findByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
import { CoreApp } from '@grafana/data';
|
import { CoreApp } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { LokiQueryEditorSelector } from '../querybuilder/components/LokiQueryEditorSelector';
|
import { LokiQueryEditorSelector } from './LokiQueryEditor';
|
||||||
|
|
||||||
import { LokiExploreQueryEditor } from './LokiExploreQueryEditor';
|
|
||||||
import { LokiQueryEditor } from './LokiQueryEditor';
|
|
||||||
import { LokiQueryEditorForAlerting } from './LokiQueryEditorForAlerting';
|
import { LokiQueryEditorForAlerting } from './LokiQueryEditorForAlerting';
|
||||||
import { LokiQueryEditorProps } from './types';
|
import { LokiQueryEditorProps } from './types';
|
||||||
|
|
||||||
@ -16,17 +12,13 @@ export function LokiQueryEditorByApp(props: LokiQueryEditorProps) {
|
|||||||
switch (app) {
|
switch (app) {
|
||||||
case CoreApp.CloudAlerting:
|
case CoreApp.CloudAlerting:
|
||||||
return <LokiQueryEditorForAlerting {...props} />;
|
return <LokiQueryEditorForAlerting {...props} />;
|
||||||
case CoreApp.Explore:
|
|
||||||
if (config.featureToggles.lokiQueryBuilder) {
|
|
||||||
return <LokiQueryEditorSelector {...props} />;
|
|
||||||
}
|
|
||||||
return <LokiExploreQueryEditor {...props} />;
|
|
||||||
default:
|
default:
|
||||||
if (config.featureToggles.lokiQueryBuilder) {
|
return <LokiQueryEditorSelector {...props} />;
|
||||||
return <LokiQueryEditorSelector {...props} />;
|
|
||||||
}
|
|
||||||
return <LokiQueryEditor {...props} />;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(LokiQueryEditorByApp);
|
export default memo(LokiQueryEditorByApp);
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
editor: 'loki-editor',
|
||||||
|
};
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
QueryBuilderOperation,
|
QueryBuilderOperation,
|
||||||
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||||
|
|
||||||
|
import { testIds } from '../../components/LokiQueryEditor';
|
||||||
import { LokiDatasource } from '../../datasource';
|
import { LokiDatasource } from '../../datasource';
|
||||||
import { escapeLabelValueInSelector } from '../../languageUtils';
|
import { escapeLabelValueInSelector } from '../../languageUtils';
|
||||||
import logqlGrammar from '../../syntax';
|
import logqlGrammar from '../../syntax';
|
||||||
@ -107,7 +108,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
|
|
||||||
const lang = { grammar: logqlGrammar, name: 'logql' };
|
const lang = { grammar: logqlGrammar, name: 'logql' };
|
||||||
return (
|
return (
|
||||||
<>
|
<div data-testid={testIds.editor}>
|
||||||
<EditorRow>
|
<EditorRow>
|
||||||
<LabelFilters
|
<LabelFilters
|
||||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||||
@ -170,7 +171,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
showExplain={showExplain}
|
showExplain={showExplain}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import React, { useEffect, useReducer } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
|
import { testIds } from '../../components/LokiQueryEditor';
|
||||||
import { LokiDatasource } from '../../datasource';
|
import { LokiDatasource } from '../../datasource';
|
||||||
import { LokiQuery } from '../../types';
|
import { LokiQuery } from '../../types';
|
||||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||||
@ -64,6 +65,7 @@ export function LokiQueryBuilderContainer(props: Props) {
|
|||||||
onChange={onVisQueryChange}
|
onChange={onVisQueryChange}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
showExplain={showExplain}
|
showExplain={showExplain}
|
||||||
|
data-testid={testIds.editor}
|
||||||
/>
|
/>
|
||||||
{showRawQuery && <QueryPreview query={query.expr} />}
|
{showRawQuery && <QueryPreview query={query.expr} />}
|
||||||
</>
|
</>
|
||||||
|
@ -37,8 +37,8 @@ export function LokiQueryCodeEditor({ query, datasource, range, onRunQuery, onCh
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
history={[]}
|
history={[]}
|
||||||
data={data}
|
data={data}
|
||||||
data-testid={testIds.editor}
|
|
||||||
app={app}
|
app={app}
|
||||||
|
data-testid={testIds.editor}
|
||||||
/>
|
/>
|
||||||
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
|
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -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';
|
|
Loading…
Reference in New Issue
Block a user