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 fix
This commit is contained in:
Peter Holmberg 2021-04-09 14:46:24 +02:00 committed by GitHub
parent 13371493ae
commit 31a8413fd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 528 additions and 196 deletions

View File

@ -43,7 +43,7 @@ export const ButtonSelect = React.memo(<T,>(props: Props<T>) => {
};
return (
<>
<div className={styles.wrapper}>
<ToolbarButton
className={className}
isOpen={isOpen}
@ -71,7 +71,7 @@ export const ButtonSelect = React.memo(<T,>(props: Props<T>) => {
</ClickOutsideWrapper>
</div>
)}
</>
</div>
);
});

View File

@ -25,12 +25,20 @@ const alertStateSortScore = {
paused: 5,
};
export enum EvalFunction {
'IsAbove' = 'gt',
'IsBelow' = 'lt',
'IsOutsideRange' = 'outside_range',
'IsWithinRange' = 'within_range',
'HasNoValue' = 'no_value',
}
const evalFunctions = [
{ text: 'IS ABOVE', value: 'gt' },
{ text: 'IS BELOW', value: 'lt' },
{ text: 'IS OUTSIDE RANGE', value: 'outside_range' },
{ text: 'IS WITHIN RANGE', value: 'within_range' },
{ text: 'HAS NO VALUE', value: 'no_value' },
{ value: EvalFunction.IsAbove, text: 'IS ABOVE' },
{ value: EvalFunction.IsBelow, text: 'IS BELOW' },
{ value: EvalFunction.IsOutsideRange, text: 'IS OUTSIDE RANGE' },
{ value: EvalFunction.IsWithinRange, text: 'IS WITHIN RANGE' },
{ value: EvalFunction.HasNoValue, text: 'HAS NO VALUE' },
];
const evalOperators = [

View File

@ -1,5 +1,5 @@
import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
import { ExpressionQuery, GELQueryType } from './types';
import { ExpressionQuery, ExpressionQueryType } from './types';
import { ExpressionQueryEditor } from './ExpressionQueryEditor';
import { DataSourceWithBackend } from '@grafana/runtime';
@ -18,7 +18,7 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
newQuery(): ExpressionQuery {
return {
refId: '--', // Replaced with query
type: GELQueryType.math,
type: ExpressionQueryType.math,
datasource: ExpressionDatasourceID,
};
}

View File

@ -1,202 +1,53 @@
// Libraries
import React, { PureComponent, ChangeEvent } from 'react';
import { css } from '@emotion/css';
import { InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
import { SelectableValue, ReducerID, QueryEditorProps } from '@grafana/data';
// Types
import { ExpressionQuery, GELQueryType } from './types';
import React, { PureComponent } from 'react';
import { SelectableValue, QueryEditorProps } from '@grafana/data';
import { InlineField, Select } from '@grafana/ui';
import { ExpressionDatasourceApi } from './ExpressionDatasource';
import { Resample } from './components/Resample';
import { Reduce } from './components/Reduce';
import { Math } from './components/Math';
import { ClassicConditions } from './components/ClassicConditions';
import { getDefaults } from './utils/expressionTypes';
import { ExpressionQuery, ExpressionQueryType, gelTypes } from './types';
type Props = QueryEditorProps<ExpressionDatasourceApi, ExpressionQuery>;
interface State {}
const gelTypes: Array<SelectableValue<GELQueryType>> = [
{ value: GELQueryType.math, label: 'Math' },
{ value: GELQueryType.reduce, label: 'Reduce' },
{ value: GELQueryType.resample, label: 'Resample' },
];
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' },
];
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' },
];
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' },
];
const mathPlaceholder =
'Math operations on one more queries, you reference the query by ${refId}, such as $A, $B, $C etc\n' +
'Example: $A + $B\n' +
'Available functions: abs(), log(), nan(), inf(), null()';
export class ExpressionQueryEditor extends PureComponent<Props, State> {
state = {};
onSelectGELType = (item: SelectableValue<GELQueryType>) => {
const labelWidth = 14;
export class ExpressionQueryEditor extends PureComponent<Props> {
onSelectExpressionType = (item: SelectableValue<ExpressionQueryType>) => {
const { query, onChange } = this.props;
const q = {
...query,
type: item.value!,
};
if (q.type === GELQueryType.reduce) {
if (!q.reducer) {
q.reducer = ReducerID.mean;
}
q.expression = undefined;
} else if (q.type === GELQueryType.resample) {
if (!q.downsampler) {
q.downsampler = ReducerID.mean;
}
if (!q.upsampler) {
q.upsampler = 'fillna';
}
q.reducer = undefined;
} else {
q.reducer = undefined;
onChange(getDefaults({ ...query, type: item.value! }));
};
renderExpressionType() {
const { onChange, query, queries } = this.props;
const refIds = queries!.filter((q) => query.refId !== q.refId).map((q) => ({ value: q.refId, label: q.refId }));
switch (query.type) {
case ExpressionQueryType.math:
return <Math onChange={onChange} query={query} labelWidth={labelWidth} />;
case ExpressionQueryType.reduce:
return <Reduce refIds={refIds} onChange={onChange} labelWidth={labelWidth} query={query} />;
case ExpressionQueryType.resample:
return <Resample query={query} labelWidth={labelWidth} onChange={onChange} refIds={refIds} />;
case ExpressionQueryType.classic:
return <ClassicConditions onChange={onChange} query={query} refIds={refIds} />;
}
onChange(q);
};
onSelectReducer = (item: SelectableValue<string>) => {
const { query, onChange } = this.props;
onChange({
...query,
reducer: item.value!,
});
};
onSelectUpsampler = (item: SelectableValue<string>) => {
const { query, onChange } = this.props;
onChange({
...query,
upsampler: item.value!,
});
};
onSelectDownsampler = (item: SelectableValue<string>) => {
const { query, onChange } = this.props;
onChange({
...query,
downsampler: item.value!,
});
};
onRuleReducer = (item: SelectableValue<string>) => {
const { query, onChange } = this.props;
onChange({
...query,
window: item.value!,
});
};
onRefIdChange = (value: SelectableValue<string>) => {
const { query, onChange } = this.props;
onChange({
...query,
expression: value.value,
});
};
onExpressionChange = (evt: ChangeEvent<any>) => {
const { query, onChange } = this.props;
onChange({
...query,
expression: evt.target.value,
});
};
onWindowChange = (evt: ChangeEvent<any>) => {
const { query, onChange } = this.props;
onChange({
...query,
window: evt.target.value,
});
};
}
render() {
const { query, queries } = this.props;
const { query } = this.props;
const selected = gelTypes.find((o) => o.value === query.type);
const reducer = reducerTypes.find((o) => o.value === query.reducer);
const downsampler = downsamplingTypes.find((o) => o.value === query.downsampler);
const upsampler = upsamplingTypes.find((o) => o.value === query.upsampler);
const labelWidth = 14;
const refIds = queries!.filter((q) => query.refId !== q.refId).map((q) => ({ value: q.refId, label: q.refId }));
return (
<div>
<InlineField label="Operation" labelWidth={labelWidth}>
<Select options={gelTypes} value={selected} onChange={this.onSelectGELType} width={25} />
<Select options={gelTypes} value={selected} onChange={this.onSelectExpressionType} width={25} />
</InlineField>
{query.type === GELQueryType.math && (
<InlineField
label="Expression"
labelWidth={labelWidth}
className={css`
align-items: baseline;
`}
>
<TextArea
value={query.expression}
onChange={this.onExpressionChange}
rows={4}
placeholder={mathPlaceholder}
/>
</InlineField>
)}
{query.type === GELQueryType.reduce && (
<InlineFieldRow>
<InlineField label="Function" labelWidth={labelWidth}>
<Select options={reducerTypes} value={reducer} onChange={this.onSelectReducer} width={25} />
</InlineField>
<InlineField label="Input" labelWidth={labelWidth}>
<Select onChange={this.onRefIdChange} options={refIds} value={query.expression} width={20} />
</InlineField>
</InlineFieldRow>
)}
{query.type === GELQueryType.resample && (
<>
<InlineFieldRow>
<InlineField label="Input" labelWidth={labelWidth}>
<Select onChange={this.onRefIdChange} options={refIds} value={query.expression} width={20} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Resample to" labelWidth={labelWidth} tooltip="10s, 1m, 30m, 1h">
<Input onChange={this.onWindowChange} value={query.window} width={15} />
</InlineField>
<InlineField label="Downsample">
<Select
options={downsamplingTypes}
value={downsampler}
onChange={this.onSelectDownsampler}
width={25}
/>
</InlineField>
<InlineField label="Upsample">
<Select options={upsamplingTypes} value={upsampler} onChange={this.onSelectUpsampler} width={25} />
</InlineField>
</InlineFieldRow>
</>
)}
{this.renderExpressionType()}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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