mirror of https://github.com/grafana/grafana
SSE: Add Classic conditions editor (#32256)
* moving expressions to components * move expression type change to util * rename gel to expressions * add clasic condition component * fix types * incremental checkin * button styling * add range inputs * some logic fixes and layout * fix remove condition * hide input if has no value * typing fixpull/32847/head
parent
13371493ae
commit
31a8413fd3
@ -0,0 +1,84 @@ |
||||
import React, { FC } from 'react'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { Button, Icon, InlineField, InlineFieldRow } from '@grafana/ui'; |
||||
import { Condition } from './Condition'; |
||||
import { ClassicCondition, ExpressionQuery } from '../types'; |
||||
import { defaultCondition } from '../utils/expressionTypes'; |
||||
|
||||
interface Props { |
||||
query: ExpressionQuery; |
||||
refIds: Array<SelectableValue<string>>; |
||||
onChange: (query: ExpressionQuery) => void; |
||||
} |
||||
|
||||
export const ClassicConditions: FC<Props> = ({ onChange, query, refIds }) => { |
||||
const onConditionChange = (condition: ClassicCondition, index: number) => { |
||||
if (query.conditions) { |
||||
onChange({ |
||||
...query, |
||||
conditions: [...query.conditions.slice(0, index), condition, ...query.conditions.slice(index + 1)], |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
const onAddCondition = () => { |
||||
if (query.conditions) { |
||||
onChange({ |
||||
...query, |
||||
conditions: query.conditions.length > 0 ? [...query.conditions, defaultCondition] : [defaultCondition], |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
const onRemoveCondition = (index: number) => { |
||||
if (query.conditions) { |
||||
const condition = query.conditions[index]; |
||||
const conditions = query.conditions |
||||
.filter((c) => c !== condition) |
||||
.map((c, index) => { |
||||
if (index === 0) { |
||||
return { |
||||
...c, |
||||
operator: { |
||||
type: 'when', |
||||
}, |
||||
}; |
||||
} |
||||
return c; |
||||
}); |
||||
onChange({ |
||||
...query, |
||||
conditions, |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<div> |
||||
<InlineFieldRow> |
||||
<InlineField label="Conditions" labelWidth={14}> |
||||
<div> |
||||
{query.conditions?.map((condition, index) => { |
||||
if (!condition) { |
||||
return; |
||||
} |
||||
return ( |
||||
<Condition |
||||
key={index} |
||||
index={index} |
||||
condition={condition} |
||||
onChange={(condition: ClassicCondition) => onConditionChange(condition, index)} |
||||
onRemoveCondition={onRemoveCondition} |
||||
refIds={refIds} |
||||
/> |
||||
); |
||||
})} |
||||
</div> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<Button variant="secondary" onClick={onAddCondition}> |
||||
<Icon name="plus-circle" /> |
||||
</Button> |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,153 @@ |
||||
import React, { FC, FormEvent } from 'react'; |
||||
import { css, cx } from '@emotion/css'; |
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data'; |
||||
import { Button, ButtonSelect, Icon, InlineFieldRow, Input, Select, useStyles } from '@grafana/ui'; |
||||
import alertDef, { EvalFunction } from '../../alerting/state/alertDef'; |
||||
import { ClassicCondition, ReducerType } from '../types'; |
||||
|
||||
interface Props { |
||||
condition: ClassicCondition; |
||||
onChange: (condition: ClassicCondition) => void; |
||||
onRemoveCondition: (id: number) => void; |
||||
index: number; |
||||
refIds: Array<SelectableValue<string>>; |
||||
} |
||||
|
||||
const reducerFunctions = alertDef.reducerTypes.map((rt) => ({ label: rt.text, value: rt.value })); |
||||
const evalOperators = alertDef.evalOperators.map((eo) => ({ label: eo.text, value: eo.value })); |
||||
const evalFunctions = alertDef.evalFunctions.map((ef) => ({ label: ef.text, value: ef.value })); |
||||
|
||||
export const Condition: FC<Props> = ({ condition, index, onChange, onRemoveCondition, refIds }) => { |
||||
const styles = useStyles(getStyles); |
||||
|
||||
const onEvalOperatorChange = (evalOperator: SelectableValue<string>) => { |
||||
onChange({ |
||||
...condition, |
||||
operator: { type: evalOperator.value! }, |
||||
}); |
||||
}; |
||||
|
||||
const onReducerFunctionChange = (conditionFunction: SelectableValue<string>) => { |
||||
onChange({ |
||||
...condition, |
||||
reducer: { type: conditionFunction.value! as ReducerType, params: [] }, |
||||
}); |
||||
}; |
||||
|
||||
const onRefIdChange = (refId: SelectableValue<string>) => { |
||||
onChange({ |
||||
...condition, |
||||
query: { params: [refId.value!] }, |
||||
}); |
||||
}; |
||||
|
||||
const onEvalFunctionChange = (evalFunction: SelectableValue<EvalFunction>) => { |
||||
onChange({ |
||||
...condition, |
||||
evaluator: { params: [], type: evalFunction.value! }, |
||||
}); |
||||
}; |
||||
|
||||
const onEvaluateValueChange = (event: FormEvent<HTMLInputElement>, index: number) => { |
||||
const newValue = parseFloat(event.currentTarget.value); |
||||
const newParams = [...condition.evaluator.params]; |
||||
newParams[index] = newValue; |
||||
|
||||
onChange({ |
||||
...condition, |
||||
evaluator: { ...condition.evaluator, params: newParams }, |
||||
}); |
||||
}; |
||||
|
||||
const buttonWidth = css` |
||||
width: 60px; |
||||
`;
|
||||
|
||||
const isRange = |
||||
condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange; |
||||
|
||||
return ( |
||||
<InlineFieldRow> |
||||
{index === 0 ? ( |
||||
<div className={cx(styles.button, buttonWidth)}>WHEN</div> |
||||
) : ( |
||||
<ButtonSelect |
||||
className={cx(styles.buttonSelectText, buttonWidth)} |
||||
options={evalOperators} |
||||
onChange={onEvalOperatorChange} |
||||
value={evalOperators.find((ea) => ea.value === condition.operator!.type)} |
||||
/> |
||||
)} |
||||
<Select |
||||
options={reducerFunctions} |
||||
onChange={onReducerFunctionChange} |
||||
width={20} |
||||
value={reducerFunctions.find((rf) => rf.value === condition.reducer.type)} |
||||
/> |
||||
<div className={styles.button}>OF</div> |
||||
<Select |
||||
onChange={onRefIdChange} |
||||
options={refIds} |
||||
width={15} |
||||
value={refIds.find((r) => r.value === condition.query.params[0])} |
||||
/> |
||||
<ButtonSelect |
||||
className={styles.buttonSelectText} |
||||
options={evalFunctions} |
||||
onChange={onEvalFunctionChange} |
||||
value={evalFunctions.find((ef) => ef.value === condition.evaluator.type)} |
||||
/> |
||||
{isRange ? ( |
||||
<> |
||||
<Input |
||||
type="number" |
||||
width={10} |
||||
onChange={(event) => onEvaluateValueChange(event, 0)} |
||||
value={condition.evaluator.params[0]} |
||||
/> |
||||
<div className={styles.button}>TO</div> |
||||
<Input |
||||
type="number" |
||||
width={10} |
||||
onChange={(event) => onEvaluateValueChange(event, 1)} |
||||
value={condition.evaluator.params[1]} |
||||
/> |
||||
</> |
||||
) : condition.evaluator.type !== EvalFunction.HasNoValue ? ( |
||||
<Input |
||||
type="number" |
||||
width={10} |
||||
onChange={(event) => onEvaluateValueChange(event, 0)} |
||||
value={condition.evaluator.params[0]} |
||||
/> |
||||
) : null} |
||||
|
||||
<Button variant="secondary" onClick={() => onRemoveCondition(index)}> |
||||
<Icon name="trash-alt" /> |
||||
</Button> |
||||
</InlineFieldRow> |
||||
); |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => { |
||||
const buttonStyle = css` |
||||
color: ${theme.colors.textBlue}; |
||||
font-size: ${theme.typography.size.sm}; |
||||
`;
|
||||
return { |
||||
buttonSelectText: buttonStyle, |
||||
button: cx( |
||||
css` |
||||
display: flex; |
||||
align-items: center; |
||||
border-radius: ${theme.border.radius.sm}; |
||||
font-weight: ${theme.typography.weight.semibold}; |
||||
border: 1px solid ${theme.colors.border1}; |
||||
white-space: nowrap; |
||||
padding: 0 ${theme.spacing.sm}; |
||||
background-color: ${theme.colors.bodyBg}; |
||||
`,
|
||||
buttonStyle |
||||
), |
||||
}; |
||||
}; |
||||
@ -0,0 +1,33 @@ |
||||
import { InlineField, TextArea } from '@grafana/ui'; |
||||
import { css } from '@emotion/css'; |
||||
import React, { ChangeEvent, FC } from 'react'; |
||||
import { ExpressionQuery } from '../types'; |
||||
|
||||
interface Props { |
||||
labelWidth: number; |
||||
query: ExpressionQuery; |
||||
onChange: (query: ExpressionQuery) => void; |
||||
} |
||||
|
||||
const mathPlaceholder = |
||||
'Math operations on one more queries, you reference the query by ${refId} ie. $A, $B, $C etc\n' + |
||||
'Example: $A + $B\n' + |
||||
'Available functions: abs(), log(), nan(), inf(), null()'; |
||||
|
||||
export const Math: FC<Props> = ({ labelWidth, onChange, query }) => { |
||||
const onExpressionChange = (event: ChangeEvent<HTMLTextAreaElement>) => { |
||||
onChange({ ...query, expression: event.target.value }); |
||||
}; |
||||
|
||||
return ( |
||||
<InlineField |
||||
label="Expression" |
||||
labelWidth={labelWidth} |
||||
className={css` |
||||
align-items: baseline; |
||||
`}
|
||||
> |
||||
<TextArea value={query.expression} onChange={onExpressionChange} rows={4} placeholder={mathPlaceholder} /> |
||||
</InlineField> |
||||
); |
||||
}; |
||||
@ -0,0 +1,34 @@ |
||||
import React, { FC } from 'react'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; |
||||
import { ExpressionQuery, reducerTypes } from '../types'; |
||||
|
||||
interface Props { |
||||
labelWidth: number; |
||||
refIds: Array<SelectableValue<string>>; |
||||
query: ExpressionQuery; |
||||
onChange: (query: ExpressionQuery) => void; |
||||
} |
||||
|
||||
export const Reduce: FC<Props> = ({ labelWidth, onChange, refIds, query }) => { |
||||
const reducer = reducerTypes.find((o) => o.value === query.reducer); |
||||
|
||||
const onRefIdChange = (value: SelectableValue<string>) => { |
||||
onChange({ ...query, expression: value.value }); |
||||
}; |
||||
|
||||
const onSelectReducer = (value: SelectableValue<string>) => { |
||||
onChange({ ...query, reducer: value.value }); |
||||
}; |
||||
|
||||
return ( |
||||
<InlineFieldRow> |
||||
<InlineField label="Function" labelWidth={labelWidth}> |
||||
<Select options={reducerTypes} value={reducer} onChange={onSelectReducer} width={25} /> |
||||
</InlineField> |
||||
<InlineField label="Input" labelWidth={labelWidth}> |
||||
<Select onChange={onRefIdChange} options={refIds} value={query.expression} width={20} /> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
); |
||||
}; |
||||
@ -0,0 +1,53 @@ |
||||
import React, { ChangeEvent, FC } from 'react'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; |
||||
import { downsamplingTypes, ExpressionQuery, upsamplingTypes } from '../types'; |
||||
|
||||
interface Props { |
||||
refIds: Array<SelectableValue<string>>; |
||||
query: ExpressionQuery; |
||||
labelWidth: number; |
||||
onChange: (query: ExpressionQuery) => void; |
||||
} |
||||
|
||||
export const Resample: FC<Props> = ({ labelWidth, onChange, refIds, query }) => { |
||||
const downsampler = downsamplingTypes.find((o) => o.value === query.downsampler); |
||||
const upsampler = upsamplingTypes.find((o) => o.value === query.upsampler); |
||||
|
||||
const onWindowChange = (event: ChangeEvent<HTMLInputElement>) => { |
||||
onChange({ ...query, window: event.target.value }); |
||||
}; |
||||
|
||||
const onRefIdChange = (value: SelectableValue<string>) => { |
||||
onChange({ ...query, expression: value.value }); |
||||
}; |
||||
|
||||
const onSelectDownsampler = (value: SelectableValue<string>) => { |
||||
onChange({ ...query, downsampler: value.value }); |
||||
}; |
||||
|
||||
const onSelectUpsampler = (value: SelectableValue<string>) => { |
||||
onChange({ ...query, upsampler: value.value }); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<InlineFieldRow> |
||||
<InlineField label="Input" labelWidth={labelWidth}> |
||||
<Select onChange={onRefIdChange} options={refIds} value={query.expression} width={20} /> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
<InlineFieldRow> |
||||
<InlineField label="Resample to" labelWidth={labelWidth} tooltip="10s, 1m, 30m, 1h"> |
||||
<Input onChange={onWindowChange} value={query.window} width={15} /> |
||||
</InlineField> |
||||
<InlineField label="Downsample"> |
||||
<Select options={downsamplingTypes} value={downsampler} onChange={onSelectDownsampler} width={25} /> |
||||
</InlineField> |
||||
<InlineField label="Upsample"> |
||||
<Select options={upsamplingTypes} value={upsampler} onChange={onSelectUpsampler} width={25} /> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
</> |
||||
); |
||||
}; |
||||
@ -1,20 +1,83 @@ |
||||
import { DataQuery } from '@grafana/data'; |
||||
import { DataQuery, ReducerID, SelectableValue } from '@grafana/data'; |
||||
import { EvalFunction } from '../alerting/state/alertDef'; |
||||
|
||||
export enum GELQueryType { |
||||
export enum ExpressionQueryType { |
||||
math = 'math', |
||||
reduce = 'reduce', |
||||
resample = 'resample', |
||||
classic = 'classic_conditions', |
||||
} |
||||
|
||||
export const gelTypes: Array<SelectableValue<ExpressionQueryType>> = [ |
||||
{ value: ExpressionQueryType.math, label: 'Math' }, |
||||
{ value: ExpressionQueryType.reduce, label: 'Reduce' }, |
||||
{ value: ExpressionQueryType.resample, label: 'Resample' }, |
||||
{ value: ExpressionQueryType.classic, label: 'Classic condition' }, |
||||
]; |
||||
|
||||
export const reducerTypes: Array<SelectableValue<string>> = [ |
||||
{ value: ReducerID.min, label: 'Min', description: 'Get the minimum value' }, |
||||
{ value: ReducerID.max, label: 'Max', description: 'Get the maximum value' }, |
||||
{ value: ReducerID.mean, label: 'Mean', description: 'Get the average value' }, |
||||
{ value: ReducerID.sum, label: 'Sum', description: 'Get the sum of all values' }, |
||||
{ value: ReducerID.count, label: 'Count', description: 'Get the number of values' }, |
||||
]; |
||||
|
||||
export const downsamplingTypes: Array<SelectableValue<string>> = [ |
||||
{ value: ReducerID.min, label: 'Min', description: 'Fill with the minimum value' }, |
||||
{ value: ReducerID.max, label: 'Max', description: 'Fill with the maximum value' }, |
||||
{ value: ReducerID.mean, label: 'Mean', description: 'Fill with the average value' }, |
||||
{ value: ReducerID.sum, label: 'Sum', description: 'Fill with the sum of all values' }, |
||||
]; |
||||
|
||||
export const upsamplingTypes: Array<SelectableValue<string>> = [ |
||||
{ value: 'pad', label: 'pad', description: 'fill with the last known value' }, |
||||
{ value: 'backfilling', label: 'backfilling', description: 'fill with the next known value' }, |
||||
{ value: 'fillna', label: 'fillna', description: 'Fill with NaNs' }, |
||||
]; |
||||
|
||||
/** |
||||
* For now this is a single object to cover all the types.... would likely |
||||
* want to split this up by type as the complexity increases |
||||
*/ |
||||
export interface ExpressionQuery extends DataQuery { |
||||
type: GELQueryType; |
||||
type: ExpressionQueryType; |
||||
reducer?: string; |
||||
expression?: string; |
||||
window?: string; |
||||
downsampler?: string; |
||||
upsampler?: string; |
||||
conditions?: ClassicCondition[]; |
||||
} |
||||
|
||||
export interface ClassicCondition { |
||||
evaluator: { |
||||
params: number[]; |
||||
type: EvalFunction; |
||||
}; |
||||
operator?: { |
||||
type: string; |
||||
}; |
||||
query: { |
||||
params: string[]; |
||||
}; |
||||
reducer: { |
||||
params: []; |
||||
type: ReducerType; |
||||
}; |
||||
type: 'query'; |
||||
} |
||||
|
||||
export type ReducerType = |
||||
| 'avg' |
||||
| 'min' |
||||
| 'max' |
||||
| 'sum' |
||||
| 'count' |
||||
| 'last' |
||||
| 'median' |
||||
| 'diff' |
||||
| 'diff_abs' |
||||
| 'percent_diff' |
||||
| 'percent_diff_abs' |
||||
| 'count_non_null'; |
||||
|
||||
@ -0,0 +1,53 @@ |
||||
import { ReducerID } from '@grafana/data'; |
||||
import { ClassicCondition, ExpressionQuery, ExpressionQueryType } from '../types'; |
||||
import { EvalFunction } from '../../alerting/state/alertDef'; |
||||
|
||||
export const getDefaults = (query: ExpressionQuery) => { |
||||
switch (query.type) { |
||||
case ExpressionQueryType.reduce: |
||||
if (!query.reducer) { |
||||
query.reducer = ReducerID.mean; |
||||
} |
||||
query.expression = undefined; |
||||
break; |
||||
|
||||
case ExpressionQueryType.resample: |
||||
if (!query.downsampler) { |
||||
query.downsampler = ReducerID.mean; |
||||
} |
||||
|
||||
if (!query.upsampler) { |
||||
query.upsampler = 'fillna'; |
||||
} |
||||
|
||||
query.reducer = undefined; |
||||
break; |
||||
|
||||
case ExpressionQueryType.classic: |
||||
if (!query.conditions) { |
||||
query.conditions = [defaultCondition]; |
||||
} |
||||
break; |
||||
|
||||
default: |
||||
query.reducer = undefined; |
||||
} |
||||
|
||||
return query; |
||||
}; |
||||
|
||||
export const defaultCondition: ClassicCondition = { |
||||
type: 'query', |
||||
reducer: { |
||||
params: [], |
||||
type: 'avg', |
||||
}, |
||||
operator: { |
||||
type: 'and', |
||||
}, |
||||
query: { params: [] }, |
||||
evaluator: { |
||||
params: [0, 0], |
||||
type: EvalFunction.IsAbove, |
||||
}, |
||||
}; |
||||
Loading…
Reference in new issue