mirror of https://github.com/grafana/grafana.git
Loki: Adds options to the new query builder (#46783)
* Initial commit for loki builder options * Loki query options * Added some basic tests * Update public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * Updated options * All option changes should trigger query * Fixed ts issue Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
This commit is contained in:
parent
3516821012
commit
1c648cb52c
|
|
@ -18,7 +18,7 @@ export interface LokiOptionFieldsProps {
|
|||
runOnBlur?: boolean;
|
||||
}
|
||||
|
||||
const queryTypeOptions: Array<SelectableValue<LokiQueryType>> = [
|
||||
export const queryTypeOptions: Array<SelectableValue<LokiQueryType>> = [
|
||||
{ value: LokiQueryType.Range, label: 'Range', description: 'Run query over a range of time.' },
|
||||
{
|
||||
value: LokiQueryType.Instant,
|
||||
|
|
@ -40,7 +40,7 @@ export const DEFAULT_RESOLUTION: SelectableValue<number> = {
|
|||
label: '1/1',
|
||||
};
|
||||
|
||||
const RESOLUTION_OPTIONS: Array<SelectableValue<number>> = [DEFAULT_RESOLUTION].concat(
|
||||
export const RESOLUTION_OPTIONS: Array<SelectableValue<number>> = [DEFAULT_RESOLUTION].concat(
|
||||
map([2, 3, 4, 5, 10], (value: number) => ({
|
||||
value,
|
||||
label: '1/' + value,
|
||||
|
|
@ -62,20 +62,6 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) {
|
|||
onChange({ ...rest, queryType });
|
||||
}
|
||||
|
||||
function preprocessMaxLines(value: string): number {
|
||||
if (value.length === 0) {
|
||||
// empty input - falls back to dataSource.maxLines limit
|
||||
return NaN;
|
||||
} else if (value.length > 0 && (isNaN(+value) || +value < 0)) {
|
||||
// input with at least 1 character and that is either incorrect (value in the input field is not a number) or negative
|
||||
// falls back to the limit of 0 lines
|
||||
return 0;
|
||||
} else {
|
||||
// default case - correct input
|
||||
return +value;
|
||||
}
|
||||
}
|
||||
|
||||
function onMaxLinesChange(e: React.SyntheticEvent<HTMLInputElement>) {
|
||||
if (query.maxLines !== preprocessMaxLines(e.currentTarget.value)) {
|
||||
onChangeQueryLimit(e.currentTarget.value);
|
||||
|
|
@ -167,3 +153,17 @@ export function LokiOptionFields(props: LokiOptionFieldsProps) {
|
|||
}
|
||||
|
||||
export default memo(LokiOptionFields);
|
||||
|
||||
export function preprocessMaxLines(value: string): number {
|
||||
if (value.length === 0) {
|
||||
// empty input - falls back to dataSource.maxLines limit
|
||||
return NaN;
|
||||
} else if (value.length > 0 && (isNaN(+value) || +value < 0)) {
|
||||
// input with at least 1 character and that is either incorrect (value in the input field is not a number) or negative
|
||||
// falls back to the limit of 0 lines
|
||||
return 0;
|
||||
} else {
|
||||
// default case - correct input
|
||||
return +value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/sh
|
|||
import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
||||
import { EditorRow, EditorRows } from '@grafana/experimental';
|
||||
import { EditorRow } from '@grafana/experimental';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
|
||||
export interface Props {
|
||||
|
|
@ -57,7 +57,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
|
|||
};
|
||||
|
||||
return (
|
||||
<EditorRows>
|
||||
<>
|
||||
<EditorRow>
|
||||
<LabelFilters
|
||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
|
|
@ -84,7 +84,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
|
|||
<QueryPreview query={query} />
|
||||
</EditorRow>
|
||||
)}
|
||||
</EditorRows>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { LokiQuery, LokiQueryType } from '../../types';
|
||||
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('LokiQueryBuilderOptions', () => {
|
||||
it('Can change query type', async () => {
|
||||
const { props } = setup();
|
||||
|
||||
screen.getByTitle('Click to edit options').click();
|
||||
expect(screen.getByLabelText('Range')).toBeChecked();
|
||||
|
||||
screen.getByLabelText('Instant').click();
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledWith({
|
||||
...props.query,
|
||||
queryType: LokiQueryType.Instant,
|
||||
});
|
||||
});
|
||||
|
||||
it('Can change legend format', async () => {
|
||||
const { props } = setup();
|
||||
|
||||
screen.getByTitle('Click to edit options').click();
|
||||
|
||||
const element = screen.getByLabelText('Legend');
|
||||
userEvent.type(element, 'asd');
|
||||
fireEvent.keyDown(element, { key: 'Enter', code: 'Enter', charCode: 13 });
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledWith({
|
||||
...props.query,
|
||||
legendFormat: 'asd',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setup(queryOverrides: Partial<LokiQuery> = {}) {
|
||||
const props = {
|
||||
query: {
|
||||
refId: 'A',
|
||||
expr: '',
|
||||
...queryOverrides,
|
||||
},
|
||||
onRunQuery: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { container } = render(<LokiQueryBuilderOptions {...props} />);
|
||||
return { container, props };
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import React from 'react';
|
||||
import { EditorRow, EditorField } from '@grafana/experimental';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { RadioButtonGroup, Select } from '@grafana/ui';
|
||||
import { LokiQuery, LokiQueryType } from '../../types';
|
||||
import { QueryOptionGroup } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup';
|
||||
import { preprocessMaxLines, queryTypeOptions, RESOLUTION_OPTIONS } from '../../components/LokiOptionFields';
|
||||
import { getLegendModeLabel } from 'app/plugins/datasource/prometheus/querybuilder/components/PromQueryLegendEditor';
|
||||
import { AutoSizeInput } from 'app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput';
|
||||
import { isMetricsQuery } from '../../datasource';
|
||||
|
||||
export interface Props {
|
||||
query: LokiQuery;
|
||||
onChange: (update: LokiQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
export const LokiQueryBuilderOptions = React.memo<Props>(({ query, onChange, onRunQuery }) => {
|
||||
const onQueryTypeChange = (value: LokiQueryType) => {
|
||||
onChange({ ...query, queryType: value });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const onResolutionChange = (option: SelectableValue<number>) => {
|
||||
onChange({ ...query, resolution: option.value });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const onLegendFormatChanged = (evt: React.FormEvent<HTMLInputElement>) => {
|
||||
onChange({ ...query, legendFormat: evt.currentTarget.value });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
function onMaxLinesChange(e: React.SyntheticEvent<HTMLInputElement>) {
|
||||
const newMaxLines = preprocessMaxLines(e.currentTarget.value);
|
||||
if (query.maxLines !== newMaxLines) {
|
||||
onChange({ ...query, maxLines: newMaxLines });
|
||||
onRunQuery();
|
||||
}
|
||||
}
|
||||
|
||||
let queryType = query.queryType ?? (query.instant ? LokiQueryType.Instant : LokiQueryType.Range);
|
||||
let showMaxLines = !isMetricsQuery(query.expr);
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<QueryOptionGroup title="Options" collapsedInfo={getCollapsedInfo(query, queryType, showMaxLines)}>
|
||||
<EditorField
|
||||
label="Legend"
|
||||
tooltip="Series name override or template. Ex. {{hostname}} will be replaced with label value for hostname."
|
||||
>
|
||||
<AutoSizeInput
|
||||
placeholder="{{label}}"
|
||||
id="loki-query-editor-legend-format"
|
||||
type="string"
|
||||
minWidth={14}
|
||||
defaultValue={query.legendFormat}
|
||||
onCommitChange={onLegendFormatChanged}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label="Type">
|
||||
<RadioButtonGroup
|
||||
id="options.query.type"
|
||||
options={queryTypeOptions}
|
||||
value={queryType}
|
||||
onChange={onQueryTypeChange}
|
||||
/>
|
||||
</EditorField>
|
||||
{showMaxLines && (
|
||||
<EditorField label="Line limit" tooltip="Upper limit for number of log lines returned by query.">
|
||||
<AutoSizeInput
|
||||
className="width-4"
|
||||
placeholder="auto"
|
||||
type="number"
|
||||
min={0}
|
||||
defaultValue={query.maxLines?.toString() ?? ''}
|
||||
onCommitChange={onMaxLinesChange}
|
||||
/>
|
||||
</EditorField>
|
||||
)}
|
||||
<EditorField label="Resolution">
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={onResolutionChange}
|
||||
options={RESOLUTION_OPTIONS}
|
||||
value={query.resolution || 1}
|
||||
aria-label="Select resolution"
|
||||
menuShouldPortal
|
||||
/>
|
||||
</EditorField>
|
||||
</QueryOptionGroup>
|
||||
</EditorRow>
|
||||
);
|
||||
});
|
||||
|
||||
function getCollapsedInfo(query: LokiQuery, queryType: LokiQueryType, showMaxLines: boolean): string[] {
|
||||
const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType);
|
||||
const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1));
|
||||
|
||||
const items: string[] = [];
|
||||
|
||||
items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`);
|
||||
|
||||
if (query.resolution) {
|
||||
items.push(`Resolution: ${resolutionLabel?.label}`);
|
||||
}
|
||||
|
||||
items.push(`Type: ${queryTypeLabel?.label}`);
|
||||
|
||||
if (showMaxLines && query.maxLines) {
|
||||
items.push(`Line limit: ${query.maxLines}`);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
LokiQueryBuilderOptions.displayName = 'LokiQueryBuilderOptions';
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { testIds } from '../../components/LokiQueryEditor';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { LokiQueryEditorProps } from '../../components/types';
|
||||
import { LokiQueryField } from '../../components/LokiQueryField';
|
||||
|
||||
export function LokiQueryCodeEditor({ query, datasource, range, onRunQuery, onChange, data }: LokiQueryEditorProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<LokiQueryField
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
range={range}
|
||||
onRunQuery={onRunQuery}
|
||||
onChange={onChange}
|
||||
history={[]}
|
||||
data={data}
|
||||
data-testid={testIds.editor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
// This wrapper styling can be removed after the old PromQueryEditor is removed.
|
||||
// This is removing margin bottom on the old legacy inline form styles
|
||||
wrapper: css`
|
||||
.gf-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { EditorHeader, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
||||
import { EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
||||
import { Button, useStyles2 } 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 React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||
import { LokiQueryEditor } from '../../components/LokiQueryEditor';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { LokiQueryEditorProps } from '../../components/types';
|
||||
import { LokiQueryType } from '../../types';
|
||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||
import { getDefaultEmptyQuery, LokiVisualQuery } from '../types';
|
||||
import { LokiQueryBuilder } from './LokiQueryBuilder';
|
||||
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplaind';
|
||||
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
|
||||
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
|
||||
|
||||
export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props) => {
|
||||
const { query, onChange, onRunQuery, data } = props;
|
||||
|
|
@ -37,11 +36,6 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
|
|||
});
|
||||
};
|
||||
|
||||
const onInstantChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||
onChange({ ...query, queryType: event.currentTarget.checked ? LokiQueryType.Instant : LokiQueryType.Range });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
// If no expr (ie new query) then default to builder
|
||||
const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder);
|
||||
|
||||
|
|
@ -60,11 +54,6 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
|
|||
>
|
||||
Run query
|
||||
</Button>
|
||||
<QueryHeaderSwitch
|
||||
label="Instant"
|
||||
value={query.queryType === LokiQueryType.Instant}
|
||||
onChange={onInstantChange}
|
||||
/>
|
||||
<InlineSelect
|
||||
value={null}
|
||||
placeholder="Query patterns"
|
||||
|
|
@ -80,16 +69,21 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
|
|||
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
|
||||
</EditorHeader>
|
||||
<Space v={0.5} />
|
||||
{editorMode === QueryEditorMode.Code && <LokiQueryEditor {...props} />}
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<LokiQueryBuilder
|
||||
datasource={props.datasource}
|
||||
query={visualQuery}
|
||||
onChange={onChangeViewModel}
|
||||
onRunQuery={props.onRunQuery}
|
||||
/>
|
||||
)}
|
||||
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={visualQuery} />}
|
||||
<EditorRows>
|
||||
{editorMode === QueryEditorMode.Code && <LokiQueryCodeEditor {...props} />}
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<LokiQueryBuilder
|
||||
datasource={props.datasource}
|
||||
query={visualQuery}
|
||||
onChange={onChangeViewModel}
|
||||
onRunQuery={props.onRunQuery}
|
||||
/>
|
||||
)}
|
||||
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={visualQuery} />}
|
||||
{editorMode !== QueryEditorMode.Explain && (
|
||||
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
)}
|
||||
</EditorRows>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
|
|||
return (
|
||||
<EditorRow>
|
||||
<QueryOptionGroup title="Options" collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel)}>
|
||||
<PromQueryLegendEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
<PromQueryLegendEditor
|
||||
legendFormat={query.legendFormat}
|
||||
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
|
||||
onRunQuery={onRunQuery}
|
||||
/>
|
||||
<EditorField
|
||||
label="Min step"
|
||||
tooltip={
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import React, { useRef } from 'react';
|
|||
import { EditorField } from '@grafana/experimental';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { LegendFormatMode, PromQuery } from '../../types';
|
||||
import { LegendFormatMode } from '../../types';
|
||||
import { AutoSizeInput } from '../shared/AutoSizeInput';
|
||||
|
||||
export interface Props {
|
||||
query: PromQuery;
|
||||
onChange: (update: PromQuery) => void;
|
||||
legendFormat: string | undefined;
|
||||
onChange: (legendFormat: string) => void;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -24,33 +24,36 @@ const legendModeOptions = [
|
|||
/**
|
||||
* Tests for this component are on the parent level (PromQueryBuilderOptions).
|
||||
*/
|
||||
export const PromQueryLegendEditor = React.memo<Props>(({ query, onChange, onRunQuery }) => {
|
||||
const mode = getLegendMode(query.legendFormat);
|
||||
export const PromQueryLegendEditor = React.memo<Props>(({ legendFormat, onChange, onRunQuery }) => {
|
||||
const mode = getLegendMode(legendFormat);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onLegendFormatChanged = (evt: React.FormEvent<HTMLInputElement>) => {
|
||||
let legendFormat = evt.currentTarget.value;
|
||||
if (legendFormat.length === 0) {
|
||||
legendFormat = LegendFormatMode.Auto;
|
||||
let newFormat = evt.currentTarget.value;
|
||||
if (newFormat.length === 0) {
|
||||
newFormat = LegendFormatMode.Auto;
|
||||
}
|
||||
|
||||
if (newFormat !== legendFormat) {
|
||||
onChange(newFormat);
|
||||
onRunQuery();
|
||||
}
|
||||
onChange({ ...query, legendFormat });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const onLegendModeChanged = (value: SelectableValue<LegendFormatMode>) => {
|
||||
switch (value.value!) {
|
||||
case LegendFormatMode.Auto:
|
||||
onChange({ ...query, legendFormat: LegendFormatMode.Auto });
|
||||
onChange(LegendFormatMode.Auto);
|
||||
break;
|
||||
case LegendFormatMode.Custom:
|
||||
onChange({ ...query, legendFormat: '{{label_name}}' });
|
||||
onChange('{{label_name}}');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.setSelectionRange(2, 12, 'forward');
|
||||
}, 10);
|
||||
break;
|
||||
case LegendFormatMode.Verbose:
|
||||
onChange({ ...query, legendFormat: '' });
|
||||
onChange('');
|
||||
break;
|
||||
}
|
||||
onRunQuery();
|
||||
|
|
@ -67,7 +70,7 @@ export const PromQueryLegendEditor = React.memo<Props>(({ query, onChange, onRun
|
|||
id="legendFormat"
|
||||
minWidth={22}
|
||||
placeholder="auto"
|
||||
defaultValue={query.legendFormat}
|
||||
defaultValue={legendFormat}
|
||||
onCommitChange={onLegendFormatChanged}
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue