mirror of https://github.com/grafana/grafana
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>pull/57344/head
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 { 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 onChange = 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 |
||||
); |
||||
|
||||
const query: LokiQuery = { |
||||
expr: '', |
||||
refId: 'A', |
||||
legendFormat: 'My Legend', |
||||
}; |
||||
datasource.languageProvider.fetchLabels = jest.fn().mockResolvedValue([]); |
||||
datasource.getDataSamples = jest.fn().mockResolvedValue([]); |
||||
|
||||
const range = createMockRequestRange('2020-01-01', '2020-01-02'); |
||||
const defaultProps = { |
||||
datasource, |
||||
query: defaultQuery, |
||||
onRunQuery: () => {}, |
||||
onChange: () => {}, |
||||
}; |
||||
|
||||
const props = { |
||||
datasource, |
||||
onChange, |
||||
onRunQuery, |
||||
query, |
||||
range, |
||||
}; |
||||
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(); |
||||
}); |
||||
|
||||
Object.assign(props, propOverrides); |
||||
it('shows builder if new query', async () => { |
||||
render( |
||||
<LokiQueryEditorSelector |
||||
{...defaultProps} |
||||
query={{ |
||||
refId: 'A', |
||||
expr: '', |
||||
}} |
||||
/> |
||||
); |
||||
await expectBuilder(); |
||||
}); |
||||
|
||||
render(<LokiQueryEditor {...props} />); |
||||
}; |
||||
it('shows code editor when code mode is set', async () => { |
||||
renderWithMode(QueryEditorMode.Code); |
||||
expectCodeEditor(); |
||||
}); |
||||
|
||||
beforeAll(() => { |
||||
const mockedBackendSrv = new BackendSrv({ |
||||
fromFetch: jest.fn(), |
||||
appEvents: new EventBusSrv(), |
||||
contextSrv: new ContextSrv(), |
||||
logout: jest.fn(), |
||||
it('shows builder when builder mode is set', async () => { |
||||
renderWithMode(QueryEditorMode.Builder); |
||||
await expectBuilder(); |
||||
}); |
||||
|
||||
setBackendSrv(mockedBackendSrv); |
||||
}); |
||||
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, |
||||
}); |
||||
}); |
||||
|
||||
describe('LokiQueryEditor', () => { |
||||
it('should render without throwing', () => { |
||||
expect(() => setup()).not.toThrow(); |
||||
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,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; |
||||
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 onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => { |
||||
const nextQuery = { ...query, legendFormat: e.currentTarget.value }; |
||||
onChange(nextQuery); |
||||
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 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> |
||||
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 ( |
||||
<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} |
||||
<> |
||||
<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} |
||||
onRunQuery={onRunQuery} |
||||
onChange={onChange} |
||||
runOnBlur={true} |
||||
onChange={onChangeInternal} |
||||
onRunQuery={props.onRunQuery} |
||||
showRawQuery={rawQuery} |
||||
showExplain={explain} |
||||
/> |
||||
{legendField} |
||||
</> |
||||
} |
||||
/> |
||||
)} |
||||
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} app={app} /> |
||||
</EditorRows> |
||||
</> |
||||
); |
||||
} |
||||
}); |
||||
|
||||
export const testIds = { |
||||
editor: 'loki-editor', |
||||
}; |
||||
LokiQueryEditorSelector.displayName = 'LokiQueryEditorSelector'; |
||||
|
@ -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