mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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};
|
||||
|
@@ -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;
|
||||
`,
|
||||
});
|
||||
|
@@ -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};
|
||||
`,
|
||||
});
|
@@ -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;
|
||||
`,
|
||||
});
|
||||
|
@@ -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};
|
||||
`,
|
||||
});
|
||||
|
@@ -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();
|
||||
});
|
||||
|
@@ -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: '',
|
||||
}),
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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',
|
||||
|
Reference in New Issue
Block a user