mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
13371493ae
commit
31a8413fd3
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
153
public/app/features/expressions/components/Condition.tsx
Normal file
153
public/app/features/expressions/components/Condition.tsx
Normal 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
|
||||
),
|
||||
};
|
||||
};
|
33
public/app/features/expressions/components/Math.tsx
Normal file
33
public/app/features/expressions/components/Math.tsx
Normal 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>
|
||||
);
|
||||
};
|
34
public/app/features/expressions/components/Reduce.tsx
Normal file
34
public/app/features/expressions/components/Reduce.tsx
Normal 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>
|
||||
);
|
||||
};
|
53
public/app/features/expressions/components/Resample.tsx
Normal file
53
public/app/features/expressions/components/Resample.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
53
public/app/features/expressions/utils/expressionTypes.ts
Normal file
53
public/app/features/expressions/utils/expressionTypes.ts
Normal 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,
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user