Commit a54e0ff7 by Ivana Huckova Committed by GitHub

Loki: Add query type and line limit to query editor in dashboard (#29356)

* WIP Add line limit and query type switch toLoki dashboard editor

* Refactor, reuse code for both - Explore and Dashboard

* Üpdate snapshot tests

* Refactor and unify

* Rename test file

* Update test
parent 460883d0
...@@ -2,7 +2,7 @@ import React from 'react'; ...@@ -2,7 +2,7 @@ import React from 'react';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import LokiExploreQueryEditor from './LokiExploreQueryEditor'; import LokiExploreQueryEditor from './LokiExploreQueryEditor';
import { LokiExploreExtraField } from './LokiExploreExtraField'; import { LokiOptionFields } from './LokiOptionFields';
import { LokiDatasource } from '../datasource'; import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types'; import { LokiQuery } from '../types';
import { ExploreMode, LoadingState, PanelData, toUtc, TimeRange } from '@grafana/data'; import { ExploreMode, LoadingState, PanelData, toUtc, TimeRange } from '@grafana/data';
...@@ -93,7 +93,7 @@ describe('LokiExploreQueryEditor', () => { ...@@ -93,7 +93,7 @@ describe('LokiExploreQueryEditor', () => {
// @ts-ignore strict null error TS2345: Argument of type '() => Promise<void>' is not assignable to parameter of type '() => void | undefined'. // @ts-ignore strict null error TS2345: Argument of type '() => Promise<void>' is not assignable to parameter of type '() => void | undefined'.
await act(async () => { await act(async () => {
const wrapper = setup(mount); const wrapper = setup(mount);
expect(wrapper.find(LokiExploreExtraField).length).toBe(1); expect(wrapper.find(LokiOptionFields).length).toBe(1);
}); });
}); });
}); });
...@@ -7,56 +7,12 @@ import { ExploreQueryFieldProps } from '@grafana/data'; ...@@ -7,56 +7,12 @@ import { ExploreQueryFieldProps } from '@grafana/data';
import { LokiDatasource } from '../datasource'; import { LokiDatasource } from '../datasource';
import { LokiQuery, LokiOptions } from '../types'; import { LokiQuery, LokiOptions } from '../types';
import { LokiQueryField } from './LokiQueryField'; import { LokiQueryField } from './LokiQueryField';
import LokiExploreExtraField from './LokiExploreExtraField';
type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>; type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>;
export function LokiExploreQueryEditor(props: Props) { export function LokiExploreQueryEditor(props: Props) {
const { range, query, data, datasource, history, onChange, onRunQuery } = props; const { range, query, data, datasource, history, onChange, onRunQuery } = props;
function onChangeQueryLimit(value: string) {
const { query, onChange } = props;
const nextQuery = { ...query, maxLines: preprocessMaxLines(value) };
onChange(nextQuery);
}
function onQueryTypeChange(value: string) {
const { query, onChange } = props;
let nextQuery;
if (value === 'instant') {
nextQuery = { ...query, instant: true, range: false };
} else {
nextQuery = { ...query, instant: false, range: true };
}
onChange(nextQuery);
}
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);
}
}
function onReturnKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
onRunQuery();
}
}
return ( return (
<LokiQueryField <LokiQueryField
datasource={datasource} datasource={datasource}
...@@ -67,15 +23,6 @@ export function LokiExploreQueryEditor(props: Props) { ...@@ -67,15 +23,6 @@ export function LokiExploreQueryEditor(props: Props) {
history={history} history={history}
data={data} data={data}
range={range} range={range}
ExtraFieldElement={
<LokiExploreExtraField
queryType={query.instant ? 'instant' : 'range'}
lineLimitValue={query?.maxLines?.toString() || ''}
onQueryTypeChange={onQueryTypeChange}
onLineLimitChange={onMaxLinesChange}
onKeyDownFunc={onReturnKeyDown}
/>
}
/> />
); );
} }
......
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { LokiExploreExtraFieldProps, LokiExploreExtraField } from './LokiExploreExtraField'; import { LokiOptionFieldsProps, LokiOptionFields } from './LokiOptionFields';
const setup = (propOverrides?: LokiExploreExtraFieldProps) => { const setup = (propOverrides?: LokiOptionFieldsProps) => {
const queryType = 'range'; const queryType = 'range';
const lineLimitValue = '1'; const lineLimitValue = '1';
const onLineLimitChange = jest.fn(); const onLineLimitChange = jest.fn();
...@@ -19,10 +19,10 @@ const setup = (propOverrides?: LokiExploreExtraFieldProps) => { ...@@ -19,10 +19,10 @@ const setup = (propOverrides?: LokiExploreExtraFieldProps) => {
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
return render(<LokiExploreExtraField {...props} />); return render(<LokiOptionFields {...props} />);
}; };
describe('LokiExploreExtraField', () => { describe('LokiOptionFields', () => {
it('should render step field', () => { it('should render step field', () => {
setup(); setup();
expect(screen.getByTestId('lineLimitField')).toBeInTheDocument(); expect(screen.getByTestId('lineLimitField')).toBeInTheDocument();
......
// Libraries // Libraries
import React, { memo } from 'react'; import React, { memo } from 'react';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { LokiQuery } from '../types';
// Types // Types
import { InlineFormLabel, RadioButtonGroup } from '@grafana/ui'; import { InlineFormLabel, RadioButtonGroup } from '@grafana/ui';
export interface LokiExploreExtraFieldProps { export interface LokiOptionFieldsProps {
lineLimitValue: string; lineLimitValue: string;
queryType: string; queryType: LokiQueryType;
onLineLimitChange: (e: React.SyntheticEvent<HTMLInputElement>) => void; query: LokiQuery;
onKeyDownFunc: (e: React.KeyboardEvent<HTMLInputElement>) => void; onChange: (value: LokiQuery) => void;
onQueryTypeChange: (value: string) => void; onRunQuery: () => void;
runOnBlur?: boolean;
} }
export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) { type LokiQueryType = 'instant' | 'range';
const { onLineLimitChange, onKeyDownFunc, lineLimitValue, queryType, onQueryTypeChange } = props;
const rangeOptions = [ const queryTypeOptions = [
{ value: 'range', label: 'Range' }, { value: 'range', label: 'Range' },
{ value: 'instant', label: 'Instant' }, { value: 'instant', label: 'Instant' },
]; ];
export function LokiOptionFields(props: LokiOptionFieldsProps) {
const { lineLimitValue, queryType, query, onRunQuery, runOnBlur, onChange } = props;
function onChangeQueryLimit(value: string) {
const nextQuery = { ...query, maxLines: preprocessMaxLines(value) };
onChange(nextQuery);
}
function onQueryTypeChange(value: LokiQueryType) {
let nextQuery;
if (value === 'instant') {
nextQuery = { ...query, instant: true, range: false };
} else {
nextQuery = { ...query, instant: false, range: true };
}
onChange(nextQuery);
}
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);
}
}
function onReturnKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
onRunQuery();
}
}
return ( return (
<div aria-label="Loki extra field" className="gf-form-inline"> <div aria-label="Loki extra field" className="gf-form-inline">
...@@ -41,7 +86,16 @@ export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) { ...@@ -41,7 +86,16 @@ export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) {
Query type Query type
</InlineFormLabel> </InlineFormLabel>
<RadioButtonGroup options={rangeOptions} value={queryType} onChange={onQueryTypeChange} /> <RadioButtonGroup
options={queryTypeOptions}
value={queryType}
onChange={(type: LokiQueryType) => {
onQueryTypeChange(type);
if (runOnBlur) {
onRunQuery();
}
}}
/>
</div> </div>
{/*Line limit field*/} {/*Line limit field*/}
<div <div
...@@ -60,13 +114,18 @@ export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) { ...@@ -60,13 +114,18 @@ export function LokiExploreExtraField(props: LokiExploreExtraFieldProps) {
className="gf-form-input width-4" className="gf-form-input width-4"
placeholder={'auto'} placeholder={'auto'}
min={0} min={0}
onChange={onLineLimitChange} onChange={onMaxLinesChange}
onKeyDown={onKeyDownFunc} onKeyDown={onReturnKeyDown}
value={lineLimitValue} value={lineLimitValue}
onBlur={() => {
if (runOnBlur) {
onRunQuery();
}
}}
/> />
</div> </div>
</div> </div>
); );
} }
export default memo(LokiExploreExtraField); export default memo(LokiOptionFields);
...@@ -38,7 +38,7 @@ const setup = (propOverrides?: object) => { ...@@ -38,7 +38,7 @@ const setup = (propOverrides?: object) => {
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
const wrapper = shallow(<LokiQueryEditor {...props} />); const wrapper = shallow(<LokiQueryEditor {...props} />);
const instance = wrapper.instance() as LokiQueryEditor; const instance = wrapper.instance();
return { return {
instance, instance,
......
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { memo } from 'react';
// Types // Types
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
import { InlineFormLabel } from '@grafana/ui'; import { InlineFormLabel } from '@grafana/ui';
import { LokiDatasource } from '../datasource'; import { LokiDatasource } from '../datasource';
import { LokiQuery } from '../types'; import { LokiQuery, LokiOptions } from '../types';
import { LokiQueryField } from './LokiQueryField'; import { LokiQueryField } from './LokiQueryField';
type Props = QueryEditorProps<LokiDatasource, LokiQuery>; type Props = QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions>;
interface State { export function LokiQueryEditor(props: Props) {
legendFormat: string; const { range, query, data, datasource, onChange, onRunQuery } = props;
}
export class LokiQueryEditor extends PureComponent<Props, State> {
// Query target to be modified and used for queries
query: LokiQuery;
constructor(props: Props) {
super(props);
// Use default query to prevent undefined input values
const defaultQuery: Partial<LokiQuery> = { expr: '', legendFormat: '' };
const query = Object.assign({}, defaultQuery, props.query);
this.query = query;
// Query target properties that are fully controlled inputs
this.state = {
// Fully controlled text inputs
legendFormat: query.legendFormat ?? '',
};
}
onFieldChange = (query: LokiQuery, override?: any) => { const onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
this.query.expr = query.expr; const nextQuery = { ...query, legendFormat: e.currentTarget.value };
onChange(nextQuery);
}; };
onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => { const legendField = (
const legendFormat = e.currentTarget.value; <div className="gf-form-inline">
this.query.legendFormat = legendFormat; <div className="gf-form">
this.setState({ legendFormat }); <InlineFormLabel
}; width={6}
tooltip="Controls the name of the time series, using name or pattern. For example
onRunQuery = () => {
const { query } = this;
this.props.onChange(query);
this.props.onRunQuery();
};
render() {
const { datasource, query, data, range } = this.props;
const { legendFormat } = this.state;
return (
<div>
<LokiQueryField
datasource={datasource}
query={query}
onChange={this.onFieldChange}
onRunQuery={this.onRunQuery}
history={[]}
data={data}
range={range}
/>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
width={7}
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." {{hostname}} will be replaced with label value for the label hostname. The legend only applies to metric queries."
> >
Legend Legend
</InlineFormLabel> </InlineFormLabel>
<input <input
type="text" type="text"
className="gf-form-input" className="gf-form-input"
placeholder="legend format" placeholder="legend format"
value={legendFormat} value={query.legendFormat || ''}
onChange={this.onLegendChange} onChange={onLegendChange}
onBlur={this.onRunQuery} onBlur={onRunQuery}
/> />
</div>
</div>
</div> </div>
); </div>
} );
return (
<LokiQueryField
datasource={datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
onBlur={onRunQuery}
history={[]}
data={data}
range={range}
runOnBlur={true}
ExtraFieldElement={legendField}
/>
);
} }
export default LokiQueryEditor; export default memo(LokiQueryEditor);
...@@ -23,6 +23,7 @@ import { LokiQuery, LokiOptions } from '../types'; ...@@ -23,6 +23,7 @@ import { LokiQuery, LokiOptions } from '../types';
import { Grammar } from 'prismjs'; import { Grammar } from 'prismjs';
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider'; import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
import LokiDatasource from '../datasource'; import LokiDatasource from '../datasource';
import LokiOptionFields from './LokiOptionFields';
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) { function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) {
if (!hasSyntax) { if (!hasSyntax) {
...@@ -70,6 +71,7 @@ export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<LokiData ...@@ -70,6 +71,7 @@ export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<LokiData
onLoadOptions: (selectedOptions: CascaderOption[]) => void; onLoadOptions: (selectedOptions: CascaderOption[]) => void;
onLabelsRefresh?: () => void; onLabelsRefresh?: () => void;
ExtraFieldElement?: ReactNode; ExtraFieldElement?: ReactNode;
runOnBlur?: boolean;
} }
export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> { export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormProps> {
...@@ -140,6 +142,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr ...@@ -140,6 +142,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
onLoadOptions, onLoadOptions,
onLabelsRefresh, onLabelsRefresh,
datasource, datasource,
runOnBlur,
} = this.props; } = this.props;
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider; const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined; const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
...@@ -177,6 +180,14 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr ...@@ -177,6 +180,14 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
/> />
</div> </div>
</div> </div>
<LokiOptionFields
queryType={query.instant ? 'instant' : 'range'}
lineLimitValue={query?.maxLines?.toString() || ''}
query={query}
onRunQuery={this.props.onRunQuery}
onChange={this.props.onChange}
runOnBlur={runOnBlur}
/>
{ExtraFieldElement} {ExtraFieldElement}
</> </>
); );
......
...@@ -2,15 +2,6 @@ ...@@ -2,15 +2,6 @@
exports[`LokiExploreQueryEditor should render component 1`] = ` exports[`LokiExploreQueryEditor should render component 1`] = `
<Component <Component
ExtraFieldElement={
<Memo(LokiExploreExtraField)
lineLimitValue="0"
onKeyDownFunc={[Function]}
onLineLimitChange={[Function]}
onQueryTypeChange={[Function]}
queryType="range"
/>
}
data={ data={
Object { Object {
"request": Object { "request": Object {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render LokiQueryEditor with legend should render 1`] = ` exports[`Render LokiQueryEditor with legend should render 1`] = `
<div> <Component
<Component ExtraFieldElement={
datasource={Object {}}
history={Array []}
onChange={[Function]}
onRunQuery={[Function]}
query={
Object {
"expr": "",
"legendFormat": "My Legend",
"refId": "A",
}
}
range={
Object {
"from": "2020-01-01T00:00:00.000Z",
"to": "2020-01-02T00:00:00.000Z",
}
}
/>
<div
className="gf-form-inline"
>
<div <div
className="gf-form" className="gf-form-inline"
> >
<Component <div
tooltip="Controls the name of the time series, using name or pattern. For example className="gf-form"
{{hostname}} will be replaced with label value for the label hostname. The legend only applies to metric queries."
width={7}
> >
Legend <Unknown
</Component> tooltip="Controls the name of the time series, using name or pattern. For example
<input {{hostname}} will be replaced with label value for the label hostname. The legend only applies to metric queries."
className="gf-form-input" width={6}
onBlur={[Function]} >
onChange={[Function]} Legend
placeholder="legend format" </Unknown>
type="text" <input
value="My Legend" className="gf-form-input"
/> onBlur={[MockFunction]}
onChange={[Function]}
placeholder="legend format"
type="text"
value="My Legend"
/>
</div>
</div> </div>
</div> }
</div> datasource={Object {}}
history={Array []}
onBlur={[MockFunction]}
onChange={[MockFunction]}
onRunQuery={[MockFunction]}
query={
Object {
"expr": "",
"legendFormat": "My Legend",
"refId": "A",
}
}
range={
Object {
"from": "2020-01-01T00:00:00.000Z",
"to": "2020-01-02T00:00:00.000Z",
}
}
runOnBlur={true}
/>
`; `;
exports[`Render LokiQueryEditor with legend should update timerange 1`] = ` exports[`Render LokiQueryEditor with legend should update timerange 1`] = `
<div> <Component
<Component ExtraFieldElement={
datasource={Object {}}
history={Array []}
onChange={[Function]}
onRunQuery={[Function]}
query={
Object {
"expr": "",
"legendFormat": "My Legend",
"refId": "A",
}
}
range={
Object {
"from": "2019-01-01T00:00:00.000Z",
"to": "2020-01-02T00:00:00.000Z",
}
}
/>
<div
className="gf-form-inline"
>
<div <div
className="gf-form" className="gf-form-inline"
> >
<Component <div
tooltip="Controls the name of the time series, using name or pattern. For example className="gf-form"
{{hostname}} will be replaced with label value for the label hostname. The legend only applies to metric queries."
width={7}
> >
Legend <Unknown
</Component> tooltip="Controls the name of the time series, using name or pattern. For example
<input {{hostname}} will be replaced with label value for the label hostname. The legend only applies to metric queries."
className="gf-form-input" width={6}
onBlur={[Function]} >
onChange={[Function]} Legend
placeholder="legend format" </Unknown>
type="text" <input
value="My Legend" className="gf-form-input"
/> onBlur={[MockFunction]}
onChange={[Function]}
placeholder="legend format"
type="text"
value="My Legend"
/>
</div>
</div> </div>
</div> }
</div> datasource={Object {}}
history={Array []}
onBlur={[MockFunction]}
onChange={[MockFunction]}
onRunQuery={[MockFunction]}
query={
Object {
"expr": "",
"legendFormat": "My Legend",
"refId": "A",
}
}
range={
Object {
"from": "2019-01-01T00:00:00.000Z",
"to": "2020-01-02T00:00:00.000Z",
}
}
runOnBlur={true}
/>
`; `;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment