diff --git a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts index 1d8459ec941..01a0a96477b 100644 --- a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts +++ b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.test.ts @@ -112,6 +112,33 @@ describe('LokiQueryModeller', () => { ).toBe('{app="grafana"} | unwrap count'); }); + it('Can render simply binary operation with scalar', () => { + expect( + modeller.renderQuery({ + labels: [{ label: 'app', op: '=', value: 'grafana' }], + operations: [{ id: LokiOperationId.MultiplyBy, params: [1000] }], + }) + ).toBe('{app="grafana"} * 1000'); + }); + + it('Can render query with simple binary query', () => { + expect( + modeller.renderQuery({ + labels: [{ label: 'app', op: '=', value: 'grafana' }], + operations: [{ id: LokiOperationId.Rate, params: ['5m'] }], + binaryQueries: [ + { + operator: '/', + query: { + labels: [{ label: 'job', op: '=', value: 'backup' }], + operations: [{ id: LokiOperationId.CountOverTime, params: ['5m'] }], + }, + }, + ], + }) + ).toBe('rate({app="grafana"} [5m]) / count_over_time({job="backup"} [5m])'); + }); + describe('On add operation handlers', () => { it('When adding function without range vector param should automatically add rate', () => { const query = { diff --git a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts index 6c15918d74a..f8507665fa8 100644 --- a/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts +++ b/public/app/plugins/datasource/loki/querybuilder/LokiQueryModeller.ts @@ -1,9 +1,9 @@ import { LokiAndPromQueryModellerBase } from '../../prometheus/querybuilder/shared/LokiAndPromQueryModellerBase'; import { QueryBuilderLabelFilter } from '../../prometheus/querybuilder/shared/types'; import { getOperationDefintions } from './operations'; -import { LokiOperationId, LokiQueryPattern, LokiVisualQuery, LokiVisualQueryOperationCategory } from './types'; +import { LokiOperationId, LokiQueryPattern, LokiVisualQueryOperationCategory } from './types'; -export class LokiQueryModeller extends LokiAndPromQueryModellerBase { +export class LokiQueryModeller extends LokiAndPromQueryModellerBase { constructor() { super(getOperationDefintions); @@ -11,7 +11,7 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase', + comparison: true, + }, + { + id: LokiOperationId.LessThan, + name: 'Less than', + sign: '<', + comparison: true, + }, + { + id: LokiOperationId.GreaterOrEqual, + name: 'Greater or equal to', + sign: '>=', + comparison: true, + }, + { + id: LokiOperationId.LessOrEqual, + name: 'Less or equal to', + sign: '<=', + comparison: true, + }, +]; + +// Not sure about this one. It could also be a more generic 'Simple math operation' where user specifies +// both the operator and the operand in a single input +export const binaryScalarOperations: QueryBuilderOperationDef[] = binaryScalarDefs.map((opDef) => { + const params: QueryBuilderOperationParamDef[] = [{ name: 'Value', type: 'number' }]; + const defaultParams: any[] = [2]; + if (opDef.comparison) { + params.unshift({ + name: 'Bool', + type: 'boolean', + description: 'If checked comparison will return 0 or 1 for the value rather than filtering.', + }); + defaultParams.unshift(false); + } + + return { + id: opDef.id, + name: opDef.name, + params, + defaultParams, + alternativesKey: 'binary scalar operations', + category: LokiVisualQueryOperationCategory.BinaryOps, + renderer: getSimpleBinaryRenderer(opDef.sign), + addOperationHandler: defaultAddOperationHandler, + }; +}); + +function getSimpleBinaryRenderer(operator: string) { + return function binaryRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) { + let param = model.params[0]; + let bool = ''; + if (model.params.length === 2) { + param = model.params[1]; + bool = model.params[0] ? ' bool' : ''; + } + + return `${innerExpr} ${operator}${bool} ${param}`; + }; +} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index ffdb5e70166..69da0eb0828 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -8,6 +8,8 @@ import { lokiQueryModeller } from '../LokiQueryModeller'; import { DataSourceApi, SelectableValue } from '@grafana/data'; import { EditorRow } from '@grafana/experimental'; import { QueryPreview } from './QueryPreview'; +import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationsEditorRow'; +import { NestedQueryList } from './NestedQueryList'; export interface Props { query: LokiVisualQuery; @@ -70,7 +72,7 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, onChange={onChangeLabels} /> - + (({ datasource, query, nested, onRunQuery={onRunQuery} datasource={datasource as DataSourceApi} /> - + + {query.binaryQueries && query.binaryQueries.length > 0 && ( + + )} {!nested && ( diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx index 2c34d60768c..7984394bdf5 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LokiDatasource } from '../../datasource'; import { cloneDeep, defaultsDeep } from 'lodash'; -import { LokiQuery } from '../../types'; +import { LokiQuery, LokiQueryType } from '../../types'; import { LokiQueryEditorSelector } from './LokiQueryEditorSelector'; import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; @@ -77,6 +77,7 @@ describe('LokiQueryEditorSelector', () => { expect(onChange).toBeCalledWith({ refId: 'A', expr: defaultQuery.expr, + queryType: LokiQueryType.Range, editorMode: QueryEditorMode.Builder, }); }); @@ -111,6 +112,7 @@ describe('LokiQueryEditorSelector', () => { expect(onChange).toBeCalledWith({ refId: 'A', expr: defaultQuery.expr, + queryType: LokiQueryType.Range, editorMode: QueryEditorMode.Code, }); }); @@ -121,6 +123,7 @@ describe('LokiQueryEditorSelector', () => { expect(onChange).toBeCalledWith({ refId: 'A', expr: defaultQuery.expr, + queryType: LokiQueryType.Range, editorMode: QueryEditorMode.Explain, }); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index 177d7dbcd89..603ca0764cd 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -7,6 +7,7 @@ import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/ import React, { useCallback, useState } from 'react'; import { LokiQueryEditorProps } from '../../components/types'; import { lokiQueryModeller } from '../LokiQueryModeller'; +import { getQueryWithDefaults } from '../state'; import { getDefaultEmptyQuery, LokiVisualQuery } from '../types'; import { LokiQueryBuilder } from './LokiQueryBuilder'; import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplaind'; @@ -14,8 +15,9 @@ import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; import { LokiQueryCodeEditor } from './LokiQueryCodeEditor'; export const LokiQueryEditorSelector = React.memo((props) => { - const { query, onChange, onRunQuery, data } = props; + const { onChange, onRunQuery, data } = props; const styles = useStyles2(getStyles); + const query = getQueryWithDefaults(props.query); const [visualQuery, setVisualQuery] = useState(query.visualQuery ?? getDefaultEmptyQuery()); const onEditorModeChange = useCallback( diff --git a/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx b/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx new file mode 100644 index 00000000000..64ba7faee5a --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx @@ -0,0 +1,125 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2, toOption } from '@grafana/data'; +import { EditorRows, FlexItem } from '@grafana/experimental'; +import { IconButton, Select, useStyles2 } from '@grafana/ui'; +import React from 'react'; +import { binaryScalarDefs } from '../binaryScalarOperations'; +import { LokiVisualQueryBinary } from '../types'; +import { LokiDatasource } from '../../datasource'; +import { LokiQueryBuilder } from './LokiQueryBuilder'; +import { AutoSizeInput } from 'app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput'; + +export interface Props { + nestedQuery: LokiVisualQueryBinary; + datasource: LokiDatasource; + index: number; + onChange: (index: number, update: LokiVisualQueryBinary) => void; + onRemove: (index: number) => void; + onRunQuery: () => void; +} + +export const NestedQuery = React.memo(({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery }) => { + const styles = useStyles2(getStyles); + + return ( +
+
+
Operator
+