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:
Torkel Ödegaard 2022-03-25 13:13:55 +01:00 committed by GitHub
parent 3516821012
commit 1c648cb52c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 266 additions and 59 deletions

View File

@ -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;
}
}

View File

@ -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>
</>
);
});

View File

@ -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 };
}

View File

@ -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';

View File

@ -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;
}
`,
};
};

View File

@ -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>
</>
);
});

View File

@ -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={

View File

@ -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}
/>