Alerting: Expression card improvements (#70395)

* Show description for each expression type in the body and change widht depending on the type

* Move condition indicator to the header

* Make order of fields in expressions to be consistent for each expression type

* Add tooltip for expression type menu

* Update styles depending on the expression type

* Update styles and move add query button under queries

* Add NeedHelpInfo component

* Adress PR review comments

* Apply description updates from #70540

* Rename gelTypes to expressionTypes

* Update layout for expressions according to the real usecases

* Update footer to include series count in all expressions

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Sonia Aguilar
2023-06-27 11:35:56 +02:00
committed by GitHub
parent c1ce24c90f
commit b0ac49926d
12 changed files with 337 additions and 138 deletions

View File

@@ -29,7 +29,7 @@ export const AlertConditionIndicator = ({ enabled = false, error, warning, onSet
if (!enabled) {
return (
<button type="button" className={styles.actionLink} onClick={() => onSetCondition && onSetCondition()}>
Make this the alert condition
Set as alert condition
</button>
);
}

View File

@@ -1,16 +1,21 @@
import { css, cx } from '@emotion/css';
import { capitalize, uniqueId } from 'lodash';
import { uniqueId } from 'lodash';
import React, { FC, useCallback, useState } from 'react';
import { DataFrame, dateTimeFormat, GrafanaTheme2, isTimeSeriesFrames, LoadingState, PanelData } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { AutoSizeInput, Button, clearButtonStyles, Icon, IconButton, Select, useStyles2 } from '@grafana/ui';
import { AutoSizeInput, Button, clearButtonStyles, IconButton, useStyles2 } from '@grafana/ui';
import { ClassicConditions } from 'app/features/expressions/components/ClassicConditions';
import { Math } from 'app/features/expressions/components/Math';
import { Reduce } from 'app/features/expressions/components/Reduce';
import { Resample } from 'app/features/expressions/components/Resample';
import { Threshold } from 'app/features/expressions/components/Threshold';
import { ExpressionQuery, ExpressionQueryType, gelTypes } from 'app/features/expressions/types';
import {
ExpressionQuery,
ExpressionQueryType,
expressionTypes,
getExpressionLabel,
} from 'app/features/expressions/types';
import { AlertQuery, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { usePagination } from '../../hooks/usePagination';
@@ -55,9 +60,10 @@ export const Expression: FC<ExpressionProps> = ({
const isLoading = data && Object.values(data).some((d) => Boolean(d) && d.state === LoadingState.Loading);
const hasResults = Array.isArray(data?.series) && !isLoading;
const series = data?.series ?? [];
const seriesCount = series.length;
const alertCondition = isAlertCondition ?? false;
const showSummary = isAlertCondition && hasResults;
//const showSummary = isAlertCondition && hasResults;
const groupedByState = {
[PromAlertingRuleState.Firing]: series.filter((serie) => getSeriesValue(serie) >= 1),
@@ -93,9 +99,18 @@ export const Expression: FC<ExpressionProps> = ({
},
[onChangeQuery, queries]
);
const selectedExpressionType = expressionTypes.find((o) => o.value === queryType);
const selectedExpressionDescription = selectedExpressionType?.description ?? '';
return (
<div className={cx(styles.expression.wrapper, alertCondition && styles.expression.alertCondition)}>
<div
className={cx(
styles.expression.wrapper,
alertCondition && styles.expression.alertCondition,
queryType === ExpressionQueryType.classic && styles.expression.classic,
queryType !== ExpressionQueryType.classic && styles.expression.nonClassic
)}
>
<div className={styles.expression.stack}>
<Header
refId={query.refId}
@@ -103,27 +118,34 @@ export const Expression: FC<ExpressionProps> = ({
onRemoveExpression={() => onRemoveExpression(query.refId)}
onUpdateRefId={(newRefId) => onUpdateRefId(query.refId, newRefId)}
onUpdateExpressionType={(type) => onUpdateExpressionType(query.refId, type)}
onSetCondition={onSetCondition}
warning={warning}
error={error}
query={query}
alertCondition={alertCondition}
/>
<div className={styles.expression.body}>{renderExpressionType(query)}</div>
{hasResults && <ExpressionResult series={series} isAlertCondition={isAlertCondition} />}
<div className={styles.footer}>
<Stack direction="row" alignItems="center">
<AlertConditionIndicator
onSetCondition={() => onSetCondition(query.refId)}
enabled={alertCondition}
error={error}
warning={warning}
/>
<Spacer />
{showSummary && (
<PreviewSummary
firing={groupedByState[PromAlertingRuleState.Firing].length}
normal={groupedByState[PromAlertingRuleState.Inactive].length}
/>
)}
</Stack>
<div className={styles.expression.body}>
<div className={styles.expression.description}>{selectedExpressionDescription}</div>
{renderExpressionType(query)}
</div>
{hasResults && (
<>
<ExpressionResult series={series} isAlertCondition={isAlertCondition} />
<div className={styles.footer}>
<Stack direction="row" alignItems="center">
<Spacer />
<PreviewSummary
isCondition={Boolean(isAlertCondition)}
firing={groupedByState[PromAlertingRuleState.Firing].length}
normal={groupedByState[PromAlertingRuleState.Inactive].length}
seriesCount={seriesCount}
/>
</Stack>
</div>
</>
)}
</div>
</div>
);
@@ -196,9 +218,17 @@ export const ExpressionResult: FC<ExpressionResultProps> = ({ series, isAlertCon
);
};
export const PreviewSummary: FC<{ firing: number; normal: number }> = ({ firing, normal }) => {
export const PreviewSummary: FC<{ firing: number; normal: number; isCondition: boolean; seriesCount: number }> = ({
firing,
normal,
isCondition,
seriesCount,
}) => {
const { mutedText } = useStyles2(getStyles);
return <span className={mutedText}>{`${firing} firing, ${normal} normal`}</span>;
if (isCondition) {
return <span className={mutedText}>{`${seriesCount} series: ${firing} firing, ${normal} normal`}</span>;
}
return <span className={mutedText}>{`${seriesCount} series`}</span>;
};
interface HeaderProps {
@@ -207,9 +237,24 @@ interface HeaderProps {
onUpdateRefId: (refId: string) => void;
onRemoveExpression: () => void;
onUpdateExpressionType: (type: ExpressionQueryType) => void;
warning?: Error;
error?: Error;
onSetCondition: (refId: string) => void;
query: ExpressionQuery;
alertCondition: boolean;
}
const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpressionType, onRemoveExpression }) => {
const Header: FC<HeaderProps> = ({
refId,
queryType,
onUpdateRefId,
onRemoveExpression,
warning,
onSetCondition,
alertCondition,
query,
error,
}) => {
const styles = useStyles2(getStyles);
const clearButton = useStyles2(clearButtonStyles);
/**
@@ -223,9 +268,6 @@ const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpr
const editing = editMode !== false;
const editingRefId = editing && editMode === 'refId';
const editingType = editing && editMode === 'expressionType';
const selectedExpressionType = gelTypes.find((o) => o.value === queryType);
return (
<header className={styles.header.wrapper}>
@@ -252,34 +294,15 @@ const Header: FC<HeaderProps> = ({ refId, queryType, onUpdateRefId, onUpdateExpr
}}
/>
)}
{!editingType && (
<button
type="button"
className={cx(clearButton, styles.editable)}
onClick={() => setEditMode('expressionType')}
>
<div className={styles.mutedText}>{capitalize(queryType)}</div>
<Icon size="xs" name="pen" className={styles.mutedIcon} onClick={() => setEditMode('expressionType')} />
</button>
)}
{editingType && (
<Select
isOpen
autoFocus
onChange={(selection) => {
onUpdateExpressionType(selection.value ?? ExpressionQueryType.classic);
setEditMode(false);
}}
onBlur={() => {
setEditMode(false);
}}
options={gelTypes}
value={selectedExpressionType}
width={25}
/>
)}
<div>{getExpressionLabel(queryType)}</div>
</Stack>
<Spacer />
<AlertConditionIndicator
onSetCondition={() => onSetCondition(query.refId)}
enabled={alertCondition}
error={error}
warning={warning}
/>
<IconButton
name="trash-alt"
variant="secondary"
@@ -357,7 +380,7 @@ const TimeseriesRow: FC<FrameProps & { index: number }> = ({ frame, index }) =>
return (
<div className={styles.expression.resultsRow}>
<Stack direction="row" gap={1} alignItems="center">
<Stack direction="row" alignItems="center">
<span className={cx(styles.mutedText, styles.expression.resultLabel)} title={name}>
{name}
</span>
@@ -396,22 +419,35 @@ const getStyles = (theme: GrafanaTheme2) => ({
expression: {
wrapper: css`
display: flex;
border: solid 1px ${theme.colors.border.weak};
border: solid 1px ${theme.colors.border.medium};
flex: 1;
flex-basis: 400px;
border-radius: ${theme.shape.borderRadius()};
max-width: 640px;
`,
stack: css`
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 0;
width: 100%;
min-width: 0; // this one is important to prevent text overflow
`,
classic: css`
max-width: 100%;
`,
nonClassic: css`
max-width: 640px;
`,
alertCondition: css``,
body: css`
padding: ${theme.spacing(1)};
flex: 1;
`,
description: css`
margin-bottom: ${theme.spacing(1)};
font-size: ${theme.typography.size.xs};
color: ${theme.colors.text.secondary};
`,
refId: css`
font-weight: ${theme.typography.fontWeightBold};
color: ${theme.colors.primary.text};

View File

@@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { PanelData } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { GrafanaTheme2, PanelData } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
import { AlertQuery } from 'app/types/unified-alerting-dto';
@@ -36,9 +37,10 @@ export const ExpressionsEditor = ({
return isExpressionQuery(query.model) ? acc.concat(query.model) : acc;
}, []);
}, [queries]);
const styles = useStyles2(getStyles);
return (
<Stack direction="row" alignItems="stretch">
<div className={styles.wrapper}>
{expressionQueries.map((query) => {
const data = panelData[query.refId];
@@ -63,6 +65,14 @@ export const ExpressionsEditor = ({
/>
);
})}
</Stack>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
display: flex;
gap: ${theme.spacing(2)};
align-content: stretch;
flex-wrap: wrap;
`,
});

View File

@@ -0,0 +1,64 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Icon, Toggletip, useStyles2 } from '@grafana/ui';
interface NeedHelpInfoProps {
contentText: string;
externalLink: string;
linkText: string;
}
export function NeedHelpInfo({ contentText, externalLink, linkText }: NeedHelpInfoProps) {
const styles = useStyles2(getStyles);
return (
<Toggletip
content={<div className={styles.mutedText}>{contentText}</div>}
title={
<Stack gap={1} direction="row">
<Icon name="question-circle" />
Define query and alert condition
</Stack>
}
footer={
<a href={externalLink} target="_blank" rel="noreferrer">
<div className={styles.infoLink}>
{linkText} <Icon name="external-link-alt" />
</div>
</a>
}
closeButton={true}
placement="bottom-start"
>
<div className={styles.helpInfo}>
<Icon name="question-circle" />
<div className={styles.helpInfoText}>Need help?</div>
</div>
</Toggletip>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
mutedText: css`
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.size.sm};
`,
helpInfo: css`
display: flex;
flex-direction: row;
align-items: center;
width: fit-content;
font-weight: ${theme.typography.fontWeightMedium};
margin-left: ${theme.spacing(1)};
font-size: ${theme.typography.size.sm};
cursor: pointer;
`,
helpInfoText: css`
margin-left: ${theme.spacing(0.5)};
text-decoration: underline;
`,
infoLink: css`
color: ${theme.colors.text.link};
`,
});

View File

@@ -50,6 +50,5 @@ const getStyles = (theme: GrafanaTheme2) => ({
container: css`
background-color: ${theme.colors.background.primary};
height: 100%;
max-width: ${theme.breakpoints.values.xxl}px;
`,
});

View File

@@ -1,12 +1,15 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useFormContext } from 'react-hook-form';
import { getDefaultRelativeTimeRange } from '@grafana/data';
import { getDefaultRelativeTimeRange, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { Alert, Button, Field, InputControl, Tooltip } from '@grafana/ui';
import { Alert, Button, Dropdown, Field, Icon, InputControl, Menu, MenuItem, Tooltip, useStyles2 } from '@grafana/ui';
import { H5 } from '@grafana/ui/src/unstable';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { ExpressionQueryType, expressionTypes } from 'app/features/expressions/types';
import { AlertQuery } from 'app/types/unified-alerting-dto';
import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
@@ -15,6 +18,7 @@ import { getDefaultOrFirstCompatibleDataSource } from '../../../utils/datasource
import { isPromOrLokiQuery } from '../../../utils/rule-form';
import { ExpressionEditor } from '../ExpressionEditor';
import { ExpressionsEditor } from '../ExpressionsEditor';
import { NeedHelpInfo } from '../NeedHelpInfo';
import { QueryEditor } from '../QueryEditor';
import { RecordingRuleEditor } from '../RecordingRuleEditor';
import { RuleEditorSection } from '../RuleEditorSection';
@@ -223,8 +227,17 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
}
}, [condition, queries, handleSetCondition]);
const onClickType = useCallback(
(type: ExpressionQueryType) => {
dispatch(addNewExpression(type));
},
[dispatch]
);
const styles = useStyles2(getStyles);
return (
<RuleEditorSection stepNo={2} title="Set a query and alert condition">
<RuleEditorSection stepNo={2} title="Define query and alert condition">
<AlertType editingExistingRule={editingExistingRule} />
{/* This is the PromQL Editor for recording rules */}
@@ -266,6 +279,21 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
{isGrafanaManagedType && (
<Stack direction="column">
{/* Data Queries */}
<Stack direction="row" gap={1} alignItems="baseline">
<div className={styles.mutedText}>
Define queries and/or expressions and then choose one of them as the alert rule condition. This is the
threshold that an alert rule must meet or exceed in order to fire.
</div>
<NeedHelpInfo
contentText={`An alert rule consists of one or more queries and expressions that select the data you want to measure.
Define queries and/or expressions and then choose one of them as the alert rule condition. This is the threshold that an alert rule must meet or exceed in order to fire.
For more information on queries and expressions, see Query and transform data.`}
externalLink={`https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/`}
linkText={`Read about query and condition`}
/>
</Stack>
<QueryEditor
queries={dataQueries}
expressions={expressionQueries}
@@ -276,7 +304,23 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
condition={condition}
onSetCondition={handleSetCondition}
/>
<Tooltip content={'You appear to have no compatible data sources'} show={noCompatibleDataSources}>
<Button
type="button"
onClick={() => {
dispatch(addNewDataQuery());
}}
variant="secondary"
aria-label={selectors.components.QueryTab.addQuery}
disabled={noCompatibleDataSources}
className={styles.addQueryButton}
>
Add query
</Button>
</Tooltip>
{/* Expression Queries */}
<H5>Expressions</H5>
<div className={styles.mutedText}>Manipulate data returned from queries with math and other operations</div>
<ExpressionsEditor
queries={queries}
panelData={queryPreviewData}
@@ -295,33 +339,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
/>
{/* action buttons */}
<Stack direction="row">
<Tooltip content={'You appear to have no compatible data sources'} show={noCompatibleDataSources}>
<Button
type="button"
icon="plus"
onClick={() => {
dispatch(addNewDataQuery());
}}
variant="secondary"
aria-label={selectors.components.QueryTab.addQuery}
disabled={noCompatibleDataSources}
>
Add query
</Button>
</Tooltip>
{config.expressionsEnabled && (
<Button
type="button"
icon="plus"
onClick={() => {
dispatch(addNewExpression());
}}
variant="secondary"
>
Add expression
</Button>
)}
{config.expressionsEnabled && <TypeSelectorButton onClickType={onClickType} />}
{isPreviewLoading && (
<Button icon="fa fa-spinner" type="button" variant="destructive" onClick={cancelQueries}>
@@ -346,3 +364,56 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
</RuleEditorSection>
);
};
function TypeSelectorButton({ onClickType }: { onClickType: (type: ExpressionQueryType) => void }) {
const newMenu = (
<Menu>
{expressionTypes.map((type) => (
<Tooltip key={type.value} content={type.description ?? ''} placement="right">
<MenuItem
key={type.value}
onClick={() => onClickType(type.value ?? ExpressionQueryType.math)}
label={type.label ?? ''}
/>
</Tooltip>
))}
</Menu>
);
return (
<Dropdown overlay={newMenu}>
<Button variant="secondary">
Add expression
<Icon name="angle-down" />
</Button>
</Dropdown>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
mutedText: css`
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.size.sm};
margin-top: ${theme.spacing(-1)};
`,
addQueryButton: css`
width: fit-content;
`,
helpInfo: css`
display: flex;
flex-direction: row;
align-items: center;
width: fit-content;
font-weight: ${theme.typography.fontWeightMedium};
margin-left: ${theme.spacing(1)};
font-size: ${theme.typography.size.sm};
cursor: pointer;
`,
helpInfoText: css`
margin-left: ${theme.spacing(0.5)};
text-decoration: underline;
`,
infoLink: css`
color: ${theme.colors.text.link};
`,
});

View File

@@ -1,7 +1,7 @@
import { getDefaultRelativeTimeRange, RelativeTimeRange } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime/src/services/__mocks__/dataSourceSrv';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { ExpressionQuery, ExpressionQueryType, ExpressionDatasourceUID } from 'app/features/expressions/types';
import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
import { defaultCondition } from 'app/features/expressions/utils/expressionTypes';
import { AlertQuery } from 'app/types/unified-alerting-dto';
@@ -113,7 +113,7 @@ describe('Query and expressions reducer', () => {
queries: [alertQuery],
};
const newState = queriesAndExpressionsReducer(initialState, addNewExpression());
const newState = queriesAndExpressionsReducer(initialState, addNewExpression(ExpressionQueryType.math));
expect(newState.queries).toHaveLength(2);
expect(newState).toMatchSnapshot();
});

View File

@@ -5,7 +5,7 @@ import { getNextRefIdChar } from 'app/core/utils/query';
import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/utils/dataSourceFromExpression';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { ExpressionQuery, ExpressionQueryType, ExpressionDatasourceUID } from 'app/features/expressions/types';
import { ExpressionDatasourceUID, ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types';
import { defaultCondition } from 'app/features/expressions/utils/expressionTypes';
import { AlertQuery } from 'app/types/unified-alerting-dto';
@@ -33,7 +33,7 @@ export const duplicateQuery = createAction<AlertQuery>('duplicateQuery');
export const addNewDataQuery = createAction('addNewDataQuery');
export const setDataQueries = createAction<AlertQuery[]>('setDataQueries');
export const addNewExpression = createAction('addNewExpression');
export const addNewExpression = createAction<ExpressionQueryType>('addNewExpression');
export const removeExpression = createAction<string>('removeExpression');
export const updateExpression = createAction<ExpressionQuery>('updateExpression');
export const updateExpressionRefId = createAction<{ oldRefId: string; newRefId: string }>('updateExpressionRefId');
@@ -98,11 +98,11 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder
// expressions actions
builder
.addCase(addNewExpression, (state) => {
.addCase(addNewExpression, (state, { payload }) => {
state.queries = addQuery(state.queries, {
datasourceUid: ExpressionDatasourceUID,
model: expressionDatasource.newQuery({
type: ExpressionQueryType.math,
type: payload,
conditions: [{ ...defaultCondition, query: { params: [] } }],
expression: '',
}),

View File

@@ -8,7 +8,7 @@ import { Math } from './components/Math';
import { Reduce } from './components/Reduce';
import { Resample } from './components/Resample';
import { Threshold } from './components/Threshold';
import { ExpressionQuery, ExpressionQueryType, gelTypes } from './types';
import { ExpressionQuery, ExpressionQueryType, expressionTypes } from './types';
import { getDefaults } from './utils/expressionTypes';
type Props = QueryEditorProps<DataSourceApi<ExpressionQuery>, ExpressionQuery>;
@@ -92,12 +92,12 @@ export function ExpressionQueryEditor(props: Props) {
}
};
const selected = gelTypes.find((o) => o.value === query.type);
const selected = expressionTypes.find((o) => o.value === query.type);
return (
<div>
<InlineField label="Operation" labelWidth={labelWidth}>
<Select options={gelTypes} value={selected} onChange={onSelectExpressionType} width={25} />
<Select options={expressionTypes} value={selected} onChange={onSelectExpressionType} width={25} />
</InlineField>
{renderExpressionType()}
</div>

View File

@@ -69,14 +69,14 @@ export const Reduce = ({ labelWidth = 'auto', onChange, refIds, query }: Props)
return (
<>
<InlineFieldRow>
<InlineField label="Function" labelWidth={labelWidth}>
<Select options={reducerTypes} value={reducer} onChange={onSelectReducer} width={20} />
</InlineField>
<InlineField label="Input" labelWidth={labelWidth}>
<Select onChange={onRefIdChange} options={refIds} value={query.expression} width={'auto'} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Function" labelWidth={labelWidth}>
<Select options={reducerTypes} value={reducer} onChange={onSelectReducer} width={20} />
</InlineField>
<InlineField label="Mode" labelWidth={labelWidth}>
<Select onChange={onModeChanged} options={reducerModes} value={mode} width={25} />
</InlineField>

View File

@@ -67,41 +67,45 @@ export const Threshold = ({ labelWidth, onChange, refIds, query }: Props) => {
condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange;
return (
<InlineFieldRow>
<InlineField label="Input" labelWidth={labelWidth}>
<Select onChange={onRefIdChange} options={refIds} value={query.expression} width={20} />
</InlineField>
<ButtonSelect
className={styles.buttonSelectText}
options={thresholdFunctions}
onChange={onEvalFunctionChange}
value={thresholdFunction}
/>
{isRange ? (
<>
<>
<InlineFieldRow>
<InlineField label="Input" labelWidth={labelWidth}>
<Select onChange={onRefIdChange} options={refIds} value={query.expression} width={20} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<ButtonSelect
className={styles.buttonSelectText}
options={thresholdFunctions}
onChange={onEvalFunctionChange}
value={thresholdFunction}
/>
{isRange ? (
<>
<Input
type="number"
width={10}
onChange={(event) => onEvaluateValueChange(event, 0)}
defaultValue={condition.evaluator.params[0]}
/>
<div className={styles.button}>TO</div>
<Input
type="number"
width={10}
onChange={(event) => onEvaluateValueChange(event, 1)}
defaultValue={condition.evaluator.params[1]}
/>
</>
) : (
<Input
type="number"
width={10}
onChange={(event) => onEvaluateValueChange(event, 0)}
defaultValue={condition.evaluator.params[0]}
defaultValue={conditions[0].evaluator.params[0] || 0}
/>
<div className={styles.button}>TO</div>
<Input
type="number"
width={10}
onChange={(event) => onEvaluateValueChange(event, 1)}
defaultValue={condition.evaluator.params[1]}
/>
</>
) : (
<Input
type="number"
width={10}
onChange={(event) => onEvaluateValueChange(event, 0)}
defaultValue={conditions[0].evaluator.params[0] || 0}
/>
)}
</InlineFieldRow>
)}
</InlineFieldRow>
</>
);
};

View File

@@ -15,7 +15,22 @@ export enum ExpressionQueryType {
threshold = 'threshold',
}
export const gelTypes: Array<SelectableValue<ExpressionQueryType>> = [
export const getExpressionLabel = (type: ExpressionQueryType) => {
switch (type) {
case ExpressionQueryType.math:
return 'Math';
case ExpressionQueryType.reduce:
return 'Reduce';
case ExpressionQueryType.resample:
return 'Resample';
case ExpressionQueryType.classic:
return 'Classic condition';
case ExpressionQueryType.threshold:
return 'Threshold';
}
};
export const expressionTypes: Array<SelectableValue<ExpressionQueryType>> = [
{
value: ExpressionQueryType.math,
label: 'Math',