mirror of https://github.com/grafana/grafana
Loki: Support binary operations and nested queries in new builder (#47012)
* Binary ops support * Add support for nested expressions in loki * Nested queries working * Fixing tests * Share more code between loki and prometheus query modellerspull/47015/head^2
parent
10d8ccc8ff
commit
feaa4a5c64
@ -0,0 +1,115 @@ |
|||||||
|
import { defaultAddOperationHandler } from '../../prometheus/querybuilder/shared/operationUtils'; |
||||||
|
import { |
||||||
|
QueryBuilderOperation, |
||||||
|
QueryBuilderOperationDef, |
||||||
|
QueryBuilderOperationParamDef, |
||||||
|
} from '../../prometheus/querybuilder/shared/types'; |
||||||
|
import { LokiOperationId, LokiVisualQueryOperationCategory } from './types'; |
||||||
|
|
||||||
|
export const binaryScalarDefs = [ |
||||||
|
{ |
||||||
|
id: LokiOperationId.Addition, |
||||||
|
name: 'Add scalar', |
||||||
|
sign: '+', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.Subtraction, |
||||||
|
name: 'Subtract scalar', |
||||||
|
sign: '-', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.MultiplyBy, |
||||||
|
name: 'Multiply by scalar', |
||||||
|
sign: '*', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.DivideBy, |
||||||
|
name: 'Divide by scalar', |
||||||
|
sign: '/', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.Modulo, |
||||||
|
name: 'Modulo by scalar', |
||||||
|
sign: '%', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.Exponent, |
||||||
|
name: 'Exponent', |
||||||
|
sign: '^', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.EqualTo, |
||||||
|
name: 'Equal to', |
||||||
|
sign: '==', |
||||||
|
comparison: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.NotEqualTo, |
||||||
|
name: 'Not equal to', |
||||||
|
sign: '!=', |
||||||
|
comparison: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: LokiOperationId.GreaterThan, |
||||||
|
name: 'Greater than', |
||||||
|
sign: '>', |
||||||
|
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}`; |
||||||
|
}; |
||||||
|
} |
@ -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<Props>(({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery }) => { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.card}> |
||||||
|
<div className={styles.header}> |
||||||
|
<div className={styles.name}>Operator</div> |
||||||
|
<Select |
||||||
|
width="auto" |
||||||
|
options={operators} |
||||||
|
value={toOption(nestedQuery.operator)} |
||||||
|
onChange={(value) => { |
||||||
|
onChange(index, { |
||||||
|
...nestedQuery, |
||||||
|
operator: value.value!, |
||||||
|
}); |
||||||
|
}} |
||||||
|
/> |
||||||
|
<div className={styles.name}>Vector matches</div> |
||||||
|
<div className={styles.vectorMatchWrapper}> |
||||||
|
<Select<LokiVisualQueryBinary['vectorMatchesType']> |
||||||
|
width="auto" |
||||||
|
value={nestedQuery.vectorMatchesType || 'on'} |
||||||
|
allowCustomValue |
||||||
|
options={[ |
||||||
|
{ value: 'on', label: 'on' }, |
||||||
|
{ value: 'ignoring', label: 'ignoring' }, |
||||||
|
]} |
||||||
|
onChange={(val) => { |
||||||
|
onChange(index, { |
||||||
|
...nestedQuery, |
||||||
|
vectorMatchesType: val.value, |
||||||
|
}); |
||||||
|
}} |
||||||
|
/> |
||||||
|
<AutoSizeInput |
||||||
|
className={styles.vectorMatchInput} |
||||||
|
minWidth={20} |
||||||
|
defaultValue={nestedQuery.vectorMatches} |
||||||
|
onCommitChange={(evt) => { |
||||||
|
onChange(index, { |
||||||
|
...nestedQuery, |
||||||
|
vectorMatches: evt.currentTarget.value, |
||||||
|
vectorMatchesType: nestedQuery.vectorMatchesType || 'on', |
||||||
|
}); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<FlexItem grow={1} /> |
||||||
|
<IconButton name="times" size="sm" onClick={() => onRemove(index)} /> |
||||||
|
</div> |
||||||
|
<div className={styles.body}> |
||||||
|
<EditorRows> |
||||||
|
<LokiQueryBuilder |
||||||
|
query={nestedQuery.query} |
||||||
|
datasource={datasource} |
||||||
|
nested={true} |
||||||
|
onRunQuery={onRunQuery} |
||||||
|
onChange={(update) => { |
||||||
|
onChange(index, { ...nestedQuery, query: update }); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</EditorRows> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
const operators = binaryScalarDefs.map((def) => ({ label: def.sign, value: def.sign })); |
||||||
|
|
||||||
|
NestedQuery.displayName = 'NestedQuery'; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => { |
||||||
|
return { |
||||||
|
card: css({ |
||||||
|
label: 'card', |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
gap: theme.spacing(0.5), |
||||||
|
}), |
||||||
|
header: css({ |
||||||
|
label: 'header', |
||||||
|
padding: theme.spacing(0.5, 0.5, 0.5, 1), |
||||||
|
gap: theme.spacing(1), |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
}), |
||||||
|
name: css({ |
||||||
|
label: 'name', |
||||||
|
whiteSpace: 'nowrap', |
||||||
|
}), |
||||||
|
body: css({ |
||||||
|
label: 'body', |
||||||
|
paddingLeft: theme.spacing(2), |
||||||
|
}), |
||||||
|
vectorMatchInput: css({ |
||||||
|
label: 'vectorMatchInput', |
||||||
|
marginLeft: -1, |
||||||
|
}), |
||||||
|
vectorMatchWrapper: css({ |
||||||
|
label: 'vectorMatchWrapper', |
||||||
|
display: 'flex', |
||||||
|
}), |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,43 @@ |
|||||||
|
import { Stack } from '@grafana/experimental'; |
||||||
|
import React from 'react'; |
||||||
|
import { LokiDatasource } from '../../datasource'; |
||||||
|
import { LokiVisualQuery, LokiVisualQueryBinary } from '../types'; |
||||||
|
import { NestedQuery } from './NestedQuery'; |
||||||
|
|
||||||
|
export interface Props { |
||||||
|
query: LokiVisualQuery; |
||||||
|
datasource: LokiDatasource; |
||||||
|
onChange: (query: LokiVisualQuery) => void; |
||||||
|
onRunQuery: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
export function NestedQueryList({ query, datasource, onChange, onRunQuery }: Props) { |
||||||
|
const nestedQueries = query.binaryQueries ?? []; |
||||||
|
|
||||||
|
const onNestedQueryUpdate = (index: number, update: LokiVisualQueryBinary) => { |
||||||
|
const updatedList = [...nestedQueries]; |
||||||
|
updatedList.splice(index, 1, update); |
||||||
|
onChange({ ...query, binaryQueries: updatedList }); |
||||||
|
}; |
||||||
|
|
||||||
|
const onRemove = (index: number) => { |
||||||
|
const updatedList = [...nestedQueries.slice(0, index), ...nestedQueries.slice(index + 1)]; |
||||||
|
onChange({ ...query, binaryQueries: updatedList }); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Stack direction="column" gap={1}> |
||||||
|
{nestedQueries.map((nestedQuery, index) => ( |
||||||
|
<NestedQuery |
||||||
|
key={index.toString()} |
||||||
|
nestedQuery={nestedQuery} |
||||||
|
index={index} |
||||||
|
onChange={onNestedQueryUpdate} |
||||||
|
datasource={datasource} |
||||||
|
onRemove={onRemove} |
||||||
|
onRunQuery={onRunQuery} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</Stack> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
import store from 'app/core/store'; |
||||||
|
import { QueryEditorMode } from '../../prometheus/querybuilder/shared/types'; |
||||||
|
import { LokiQuery, LokiQueryType } from '../types'; |
||||||
|
|
||||||
|
const queryEditorModeDefaultLocalStorageKey = 'LokiQueryEditorModeDefault'; |
||||||
|
|
||||||
|
export function changeEditorMode(query: LokiQuery, editorMode: QueryEditorMode, onChange: (query: LokiQuery) => void) { |
||||||
|
// If empty query store new mode as default
|
||||||
|
if (query.expr === '') { |
||||||
|
store.set(queryEditorModeDefaultLocalStorageKey, editorMode); |
||||||
|
} |
||||||
|
|
||||||
|
onChange({ ...query, editorMode }); |
||||||
|
} |
||||||
|
|
||||||
|
export function getDefaultEditorMode(expr: string) { |
||||||
|
// If we already have an expression default to code view
|
||||||
|
if (expr != null && expr !== '') { |
||||||
|
return QueryEditorMode.Code; |
||||||
|
} |
||||||
|
|
||||||
|
const value = store.get(queryEditorModeDefaultLocalStorageKey) as QueryEditorMode; |
||||||
|
switch (value) { |
||||||
|
case QueryEditorMode.Builder: |
||||||
|
case QueryEditorMode.Code: |
||||||
|
case QueryEditorMode.Explain: |
||||||
|
return value; |
||||||
|
default: |
||||||
|
return QueryEditorMode.Builder; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns query with defaults, and boolean true/false depending on change was required |
||||||
|
*/ |
||||||
|
export function getQueryWithDefaults(query: LokiQuery): LokiQuery { |
||||||
|
// If no expr (ie new query) then default to builder
|
||||||
|
let result = query; |
||||||
|
|
||||||
|
if (!query.editorMode) { |
||||||
|
result = { ...query, editorMode: getDefaultEditorMode(query.expr) }; |
||||||
|
} |
||||||
|
|
||||||
|
if (query.expr == null) { |
||||||
|
result = { ...result, expr: '' }; |
||||||
|
} |
||||||
|
|
||||||
|
if (query.queryType == null) { |
||||||
|
// Default to range query
|
||||||
|
result = { ...result, queryType: LokiQueryType.Range }; |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
Loading…
Reference in new issue