diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx index 2d478ca0293..828e36b16a5 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx @@ -1,6 +1,7 @@ import { css, cx } from '@emotion/css'; import React, { FC, FormEvent, ReactNode, useCallback, useEffect, useState } from 'react'; import Calendar from 'react-calendar'; +import { usePopper } from 'react-popper'; import { useMedia } from 'react-use'; import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data'; @@ -33,6 +34,13 @@ export const DateTimePicker: FC = ({ date, maxDate, label, onChange }) => const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`); const styles = useStyles2(getStyles); + const [markerElement, setMarkerElement] = useState(); + const [selectorElement, setSelectorElement] = useState(); + + const popper = usePopper(markerElement, selectorElement, { + placement: 'bottom-start', + }); + const onApply = useCallback( (date: DateTime) => { setOpen(false); @@ -51,18 +59,29 @@ export const DateTimePicker: FC = ({ date, maxDate, label, onChange }) => return (
- + {isOpen ? ( isFullscreen ? ( - setOpen(false)}> - setOpen(false)} - maxDate={maxDate} - /> - + + setOpen(false)}> + setOpen(false)} + maxDate={maxDate} + ref={setSelectorElement} + style={popper.styles.popper} + /> + + ) : ( setOpen(false)}> @@ -84,6 +103,7 @@ interface DateTimeCalendarProps { onClose: () => void; isFullscreen: boolean; maxDate?: Date; + style?: React.CSSProperties; } interface InputProps { @@ -99,127 +119,141 @@ type InputState = { invalid: boolean; }; -const DateTimeInput: FC = ({ date, label, onChange, isFullscreen, onOpen }) => { - const [internalDate, setInternalDate] = useState(() => { - return { value: date ? dateTimeFormat(date) : dateTimeFormat(dateTime()), invalid: false }; - }); - - useEffect(() => { - if (date) { - setInternalDate({ - invalid: !isValid(dateTimeFormat(date)), - value: isDateTime(date) ? dateTimeFormat(date) : date, - }); - } - }, [date]); - - const onChangeDate = useCallback((event: FormEvent) => { - const isInvalid = !isValid(event.currentTarget.value); - setInternalDate({ - value: event.currentTarget.value, - invalid: isInvalid, +const DateTimeInput = React.forwardRef( + ({ date, label, onChange, isFullscreen, onOpen }, ref) => { + const [internalDate, setInternalDate] = useState(() => { + return { value: date ? dateTimeFormat(date) : dateTimeFormat(dateTime()), invalid: false }; }); - }, []); - const onFocus = useCallback( - (event: FormEvent) => { - if (!isFullscreen) { - return; + useEffect(() => { + if (date) { + setInternalDate({ + invalid: !isValid(dateTimeFormat(date)), + value: isDateTime(date) ? dateTimeFormat(date) : date, + }); } - onOpen(event); - }, - [isFullscreen, onOpen] - ); + }, [date]); - const onBlur = useCallback(() => { - if (isDateTime(internalDate.value)) { - onChange(dateTime(internalDate.value)); - } - }, [internalDate.value, onChange]); - - const icon = + +
- - - - - - ); -}; + ); + } +); + +DateTimeCalendar.displayName = 'DateTimeCalendar'; const getStyles = (theme: GrafanaTheme2) => ({ container: css` diff --git a/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx new file mode 100644 index 00000000000..0b2cfde30f2 --- /dev/null +++ b/public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx @@ -0,0 +1,445 @@ +import { css, cx } from '@emotion/css'; +import { dump } from 'js-yaml'; +import { keyBy, startCase } from 'lodash'; +import React from 'react'; + +import { DataSourceInstanceSettings, GrafanaTheme2, PanelData, RelativeTimeRange } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; +import { Badge, useStyles2 } from '@grafana/ui'; +import { mapRelativeTimeRangeToOption } from '@grafana/ui/src/components/DateTimePickers/RelativeTimeRangePicker/utils'; + +import { AlertQuery } from '../../../types/unified-alerting-dto'; +import { isExpressionQuery } from '../../expressions/guards'; +import { + downsamplingTypes, + ExpressionQuery, + ExpressionQueryType, + reducerModes, + ReducerMode, + reducerTypes, + thresholdFunctions, + upsamplingTypes, +} from '../../expressions/types'; +import alertDef, { EvalFunction } from '../state/alertDef'; + +import { ExpressionResult } from './components/expressions/Expression'; +import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization'; + +interface GrafanaRuleViewerProps { + queries: AlertQuery[]; + condition: string; + evalDataByQuery?: Record; + evalTimeRanges?: Record; + onTimeRangeChange: (queryRef: string, timeRange: RelativeTimeRange) => void; +} + +export function GrafanaRuleQueryViewer({ + queries, + condition, + evalDataByQuery = {}, + evalTimeRanges = {}, + onTimeRangeChange, +}: GrafanaRuleViewerProps) { + const dsByUid = keyBy(Object.values(config.datasources), (ds) => ds.uid); + const dataQueries = queries.filter((q) => !isExpressionQuery(q.model)); + const expressions = queries.filter((q) => isExpressionQuery(q.model)); + + return ( + + + {dataQueries.map(({ model, relativeTimeRange, refId, datasourceUid }, index) => { + const dataSource = dsByUid[datasourceUid]; + + return ( + onTimeRangeChange(refId, timeRange)} + /> + ); + })} + + + + {expressions.map(({ model, relativeTimeRange, refId, datasourceUid }, index) => { + const dataSource = dsByUid[datasourceUid]; + + return ( + isExpressionQuery(model) && ( + + ) + ); + })} + + + ); +} + +interface QueryPreviewProps extends Pick { + isAlertCondition: boolean; + dataSource?: DataSourceInstanceSettings; + queryData?: PanelData; + evalTimeRange?: RelativeTimeRange; + onEvalTimeRangeChange: (timeRange: RelativeTimeRange) => void; +} + +export function QueryPreview({ + refId, + relativeTimeRange, + model, + dataSource, + queryData, + evalTimeRange, + onEvalTimeRangeChange, +}: QueryPreviewProps) { + const styles = useStyles2(getQueryPreviewStyles); + + // relativeTimeRange is what is defined for a query + // evalTimeRange is temporary value which the user can change + const headerItems = [dataSource?.name ?? '[[Data source not found]]']; + if (relativeTimeRange) { + headerItems.push(mapRelativeTimeRangeToOption(relativeTimeRange).display); + } + + return ( + +
+        {dump(model)}
+      
+ {dataSource && ( + + )} +
+ ); +} + +const getQueryPreviewStyles = (theme: GrafanaTheme2) => ({ + code: css` + margin: ${theme.spacing(1)}; + `, + contentBox: css` + flex: 1 0 100%; // RuleViewerVisualization uses AutoSizer which doesn't expand the box + `, + visualization: css` + padding: ${theme.spacing(1)}; + `, +}); + +interface ExpressionPreviewProps extends Pick { + isAlertCondition: boolean; + model: ExpressionQuery; + dataSource: DataSourceInstanceSettings; + evalData?: PanelData; +} + +function ExpressionPreview({ refId, model, evalData, isAlertCondition }: ExpressionPreviewProps) { + function renderPreview() { + switch (model.type) { + case ExpressionQueryType.math: + return ; + + case ExpressionQueryType.reduce: + return ; + + case ExpressionQueryType.resample: + return ; + + case ExpressionQueryType.classic: + return ; + + case ExpressionQueryType.threshold: + return ; + + default: + return <>Expression not supported: {model.type}; + } + } + + return ( + + {renderPreview()} + {evalData && } + + ); +} + +interface QueryBoxProps extends React.PropsWithChildren { + refId: string; + headerItems?: string[]; + isAlertCondition?: boolean; + className?: string; +} + +function QueryBox({ refId, headerItems = [], children, isAlertCondition, className }: QueryBoxProps) { + const styles = useStyles2(getQueryBoxStyles); + + return ( +
+
+ {refId} + {headerItems.map((item, index) => ( + + {item} + + ))} + {isAlertCondition && ( +
+ +
+ )} +
+ {children} +
+ ); +} + +const getQueryBoxStyles = (theme: GrafanaTheme2) => ({ + container: css` + flex: 1 0 25%; + border: 1px solid ${theme.colors.border.strong}; + max-width: 100%; + `, + header: css` + display: flex; + align-items: center; + gap: ${theme.spacing(1)}; + padding: ${theme.spacing(1)}; + background-color: ${theme.colors.background.secondary}; + `, + textBlock: css` + border: 1px solid ${theme.colors.border.weak}; + padding: ${theme.spacing(0.5, 1)}; + background-color: ${theme.colors.background.primary}; + `, + refId: css` + color: ${theme.colors.text.link}; + padding: ${theme.spacing(0.5, 1)}; + border: 1px solid ${theme.colors.border.weak}; + `, + conditionIndicator: css` + margin-left: auto; + `, +}); + +function ClassicConditionViewer({ model }: { model: ExpressionQuery }) { + const styles = useStyles2(getClassicConditionViewerStyles); + + const reducerFunctions = keyBy(alertDef.reducerTypes, (rt) => rt.value); + const evalOperators = keyBy(alertDef.evalOperators, (eo) => eo.value); + const evalFunctions = keyBy(alertDef.evalFunctions, (ef) => ef.value); + + return ( +
+ {model.conditions?.map(({ query, operator, reducer, evaluator }, index) => { + const isRange = isRangeEvaluator(evaluator); + + return ( + +
+ {index === 0 ? 'WHEN' : !!operator?.type && evalOperators[operator?.type]?.text} +
+
{reducer?.type && reducerFunctions[reducer.type]?.text}
+
OF
+
{query.params[0]}
+
{evalFunctions[evaluator.type].text}
+
+ {isRange ? `(${evaluator.params[0]}; ${evaluator.params[1]})` : evaluator.params[0]} +
+
+ ); + })} +
+ ); +} + +const getClassicConditionViewerStyles = (theme: GrafanaTheme2) => ({ + container: css` + padding: ${theme.spacing(1)}; + display: grid; + grid-template-columns: max-content max-content max-content max-content max-content max-content; + gap: ${theme.spacing(0, 1)}; + `, + ...getCommonQueryStyles(theme), +}); + +function ReduceConditionViewer({ model }: { model: ExpressionQuery }) { + const styles = useStyles2(getReduceConditionViewerStyles); + + const { reducer, expression, settings } = model; + const reducerType = reducerTypes.find((rt) => rt.value === reducer); + + const reducerMode = settings?.mode ?? ReducerMode.Strict; + const modeName = reducerModes.find((rm) => rm.value === reducerMode); + + return ( +
+
Function
+
{reducerType?.label}
+ +
Input
+
{expression}
+ +
Mode
+
{modeName?.label}
+
+ ); +} + +const getReduceConditionViewerStyles = (theme: GrafanaTheme2) => ({ + container: css` + padding: ${theme.spacing(1)}; + display: grid; + gap: ${theme.spacing(1)}; + grid-template-rows: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr; + + > :nth-child(6) { + grid-column: span 3; + } + `, + ...getCommonQueryStyles(theme), +}); + +function ResampleExpressionViewer({ model }: { model: ExpressionQuery }) { + const styles = useStyles2(getResampleExpressionViewerStyles); + + const { expression, window, downsampler, upsampler } = model; + const downsamplerType = downsamplingTypes.find((dt) => dt.value === downsampler); + const upsamplerType = upsamplingTypes.find((ut) => ut.value === upsampler); + + return ( +
+
Input
+
{expression}
+ +
Resample to
+
{window}
+ +
Downsample
+
{downsamplerType?.label}
+ +
Upsample
+
{upsamplerType?.label}
+
+ ); +} + +const getResampleExpressionViewerStyles = (theme: GrafanaTheme2) => ({ + container: css` + padding: ${theme.spacing(1)}; + display: grid; + gap: ${theme.spacing(1)}; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; + `, + ...getCommonQueryStyles(theme), +}); + +function ThresholdExpressionViewer({ model }: { model: ExpressionQuery }) { + const styles = useStyles2(getExpressionViewerStyles); + + const { expression, conditions } = model; + + const evaluator = conditions && conditions[0]?.evaluator; + const thresholdFunction = thresholdFunctions.find((tf) => tf.value === evaluator?.type); + + const isRange = evaluator ? isRangeEvaluator(evaluator) : false; + + return ( +
+
Input
+
{expression}
+ + {evaluator && ( + <> +
{thresholdFunction?.label}
+
+ {isRange ? `(${evaluator.params[0]}; ${evaluator.params[1]})` : evaluator.params[0]} +
+ + )} +
+ ); +} + +const getExpressionViewerStyles = (theme: GrafanaTheme2) => { + const { blue, bold, ...common } = getCommonQueryStyles(theme); + + return { + ...common, + container: css` + padding: ${theme.spacing(1)}; + display: flex; + gap: ${theme.spacing(1)}; + `, + blue: css` + ${blue}; + margin: auto 0; + `, + bold: css` + ${bold}; + margin: auto 0; + `, + }; +}; + +function MathExpressionViewer({ model }: { model: ExpressionQuery }) { + const styles = useStyles2(getExpressionViewerStyles); + + const { expression } = model; + + return ( +
+
Input
+
{expression}
+
+ ); +} + +const getCommonQueryStyles = (theme: GrafanaTheme2) => ({ + blue: css` + color: ${theme.colors.text.link}; + `, + bold: css` + font-weight: ${theme.typography.fontWeightBold}; + `, + label: css` + display: flex; + align-items: center; + padding: ${theme.spacing(0.5, 1)}; + background-color: ${theme.colors.background.secondary}; + font-size: ${theme.typography.bodySmall.fontSize}; + line-height: ${theme.typography.bodySmall.lineHeight}; + font-weight: ${theme.typography.fontWeightBold}; + `, + value: css` + padding: ${theme.spacing(0.5, 1)}; + border: 1px solid ${theme.colors.border.weak}; + `, +}); + +function isRangeEvaluator(evaluator: { params: number[]; type: EvalFunction }) { + return evaluator.type === EvalFunction.IsWithinRange || evaluator.type === EvalFunction.IsOutsideRange; +} diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 4bfdaa11dd8..800f1db9a45 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -1,14 +1,16 @@ import { css } from '@emotion/css'; +import produce from 'immer'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useObservable } from 'react-use'; +import { useObservable, useToggle } from 'react-use'; -import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data'; +import { GrafanaTheme2, LoadingState, PanelData, RelativeTimeRange } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { Alert, Button, + Collapse, Icon, LoadingPlaceholder, - PanelChromeLoadingIndicator, useStyles2, VerticalGroup, withErrorBoundary, @@ -18,11 +20,11 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants'; import { AlertQuery } from '../../../types/unified-alerting-dto'; +import { GrafanaRuleQueryViewer, QueryPreview } from './GrafanaRuleQueryViewer'; import { AlertLabels } from './components/AlertLabels'; import { DetailsField } from './components/DetailsField'; import { ProvisionedResource, ProvisioningAlert } from './components/Provisioning'; import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout'; -import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization'; import { RuleDetailsActionButtons } from './components/rules/RuleDetailsActionButtons'; import { RuleDetailsAnnotations } from './components/rules/RuleDetailsAnnotations'; import { RuleDetailsDataSources } from './components/rules/RuleDetailsDataSources'; @@ -47,47 +49,59 @@ const pageTitle = 'View rule'; export function RuleViewer({ match }: RuleViewerProps) { const styles = useStyles2(getStyles); + const [expandQuery, setExpandQuery] = useToggle(false); + const { id } = match.params; const identifier = ruleId.tryParse(id, true); const { loading, error, result: rule } = useCombinedRule(identifier, identifier?.ruleSourceName); const runner = useMemo(() => new AlertingQueryRunner(), []); const data = useObservable(runner.get()); - const queries2 = useMemo(() => alertRuleToQueries(rule), [rule]); - const [queries, setQueries] = useState([]); + const queries = useMemo(() => alertRuleToQueries(rule), [rule]); - const { allDataSourcesAvailable } = useAlertQueriesStatus(queries2); + const [evaluationTimeRanges, setEvaluationTimeRanges] = useState>({}); + + const { allDataSourcesAvailable } = useAlertQueriesStatus(queries); const onRunQueries = useCallback(() => { if (queries.length > 0 && allDataSourcesAvailable) { - runner.run(queries); + const evalCustomizedQueries = queries.map((q) => ({ + ...q, + relativeTimeRange: evaluationTimeRanges[q.refId] ?? q.relativeTimeRange, + })); + + runner.run(evalCustomizedQueries); } - }, [queries, runner, allDataSourcesAvailable]); + }, [queries, evaluationTimeRanges, runner, allDataSourcesAvailable]); useEffect(() => { - setQueries(queries2); - }, [queries2]); + const alertQueries = alertRuleToQueries(rule); + const defaultEvalTimeRanges = Object.fromEntries( + alertQueries.map((q) => [q.refId, q.relativeTimeRange ?? { from: 0, to: 0 }]) + ); + + setEvaluationTimeRanges(defaultEvalTimeRanges); + }, [rule]); useEffect(() => { - if (allDataSourcesAvailable) { + if (allDataSourcesAvailable && expandQuery) { onRunQueries(); } - }, [onRunQueries, allDataSourcesAvailable]); + }, [onRunQueries, allDataSourcesAvailable, expandQuery]); useEffect(() => { return () => runner.destroy(); }, [runner]); - const onChangeQuery = useCallback((query: AlertQuery) => { - setQueries((queries) => - queries.map((q) => { - if (q.refId === query.refId) { - return query; - } - return q; - }) - ); - }, []); + const onQueryTimeRangeChange = useCallback( + (refId: string, timeRange: RelativeTimeRange) => { + const newEvalTimeRanges = produce(evaluationTimeRanges, (draft) => { + draft[refId] = timeRange; + }); + setEvaluationTimeRanges(newEvalTimeRanges); + }, + [evaluationTimeRanges, setEvaluationTimeRanges] + ); if (!identifier?.ruleSourceName) { return ( @@ -183,33 +197,49 @@ export function RuleViewer({ match }: RuleViewerProps) { - {!isFederatedRule && data && Object.keys(data).length > 0 && ( - <> -
- Query results runner.cancel()} /> + + {isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && ( + + )} + + {!isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && data && Object.keys(data).length > 0 && ( +
+ {queries.map((query) => { + return ( + ds.uid === query.datasourceUid)} + queryData={data[query.refId]} + relativeTimeRange={query.relativeTimeRange} + evalTimeRange={evaluationTimeRanges[query.refId]} + onEvalTimeRangeChange={(timeRange) => onQueryTimeRangeChange(query.refId, timeRange)} + isAlertCondition={false} + /> + ); + })}
- -
- {queries.map((query) => { - return ( -
- -
- ); - })} -
-
- - )} - {!isFederatedRule && !allDataSourcesAvailable && ( - - Cannot display the query preview. Some of the data sources used in the queries are not available. - - )} + )} + {!isFederatedRule && !allDataSourcesAvailable && ( + + Cannot display the query preview. Some of the data sources used in the queries are not available. + + )} +
); } @@ -227,6 +257,11 @@ const getStyles = (theme: GrafanaTheme2) => { height: 100%; width: 100%; `, + collapse: css` + margin-top: ${theme.spacing(2)}; + border-color: ${theme.colors.border.weak}; + border-radius: ${theme.shape.borderRadius()}; + `, queriesTitle: css` padding: ${theme.spacing(2, 0.5)}; font-size: ${theme.typography.h5.fontSize}; diff --git a/public/app/features/alerting/unified/components/expressions/AlertConditionIndicator.tsx b/public/app/features/alerting/unified/components/expressions/AlertConditionIndicator.tsx index 841b3ece418..f48efde64b7 100644 --- a/public/app/features/alerting/unified/components/expressions/AlertConditionIndicator.tsx +++ b/public/app/features/alerting/unified/components/expressions/AlertConditionIndicator.tsx @@ -8,7 +8,7 @@ interface AlertConditionProps { enabled?: boolean; error?: Error; warning?: Error; - onSetCondition: () => void; + onSetCondition?: () => void; } export const AlertConditionIndicator: FC = ({ @@ -33,7 +33,7 @@ export const AlertConditionIndicator: FC = ({ if (!enabled) { return ( -
onSetCondition()}> +
onSetCondition && onSetCondition()}> Make this the alert condition
); diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 295b8376db8..01b898564eb 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -56,10 +56,6 @@ export const Expression: FC = ({ const hasResults = Array.isArray(data?.series) && !isLoading; const series = data?.series ?? []; - // sometime we receive results where every value is just "null" when noData occurs - const emptyResults = hasResults && isEmptySeries(series); - const isTimeSeriesResults = !emptyResults && isTimeSeries(series); - const alertCondition = isAlertCondition ?? false; const showSummary = isAlertCondition && hasResults; @@ -109,24 +105,7 @@ export const Expression: FC = ({ onUpdateExpressionType={(type) => onUpdateExpressionType(query.refId, type)} />
{renderExpressionType(query)}
- {hasResults && ( -
- {!emptyResults && isTimeSeriesResults && ( -
- {series.map((frame, index) => ( - - ))} -
- )} - {!emptyResults && - !isTimeSeriesResults && - series.map((frame, index) => ( - // There's no way to uniquely identify a frame that doesn't cause render bugs :/ (Gilles) - - ))} - {emptyResults &&
No data
} -
- )} + {hasResults && }
= ({ ); }; -const PreviewSummary: FC<{ firing: number; normal: number }> = ({ firing, normal }) => { +interface ExpressionResultProps { + series: DataFrame[]; + isAlertCondition?: boolean; +} + +export const ExpressionResult: FC = ({ series, isAlertCondition }) => { + const styles = useStyles2(getStyles); + + // sometimes we receive results where every value is just "null" when noData occurs + const emptyResults = isEmptySeries(series); + const isTimeSeriesResults = !emptyResults && isTimeSeries(series); + + return ( +
+ {!emptyResults && isTimeSeriesResults && ( +
+ {series.map((frame, index) => ( + + ))} +
+ )} + {!emptyResults && + !isTimeSeriesResults && + series.map((frame, index) => ( + // There's no way to uniquely identify a frame that doesn't cause render bugs :/ (Gilles) + + ))} + {emptyResults &&
No data
} +
+ ); +}; + +export const PreviewSummary: FC<{ firing: number; normal: number }> = ({ firing, normal }) => { const { mutedText } = useStyles2(getStyles); return {`${firing} firing, ${normal} normal`}; }; diff --git a/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx index 09541382bcd..b0c9efea51b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/VizWrapper.tsx @@ -16,8 +16,8 @@ interface Props { data: PanelData; currentPanel: SupportedPanelPlugins; changePanel: (panel: SupportedPanelPlugins) => void; - thresholds: ThresholdsConfig; - onThresholdsChange: (thresholds: ThresholdsConfig) => void; + thresholds?: ThresholdsConfig; + onThresholdsChange?: (thresholds: ThresholdsConfig) => void; } type PanelFieldConfig = FieldConfigSource; @@ -30,7 +30,7 @@ export const VizWrapper: FC = ({ data, currentPanel, changePanel, onThres const vizHeight = useVizHeight(data, currentPanel, options.frameIndex); const styles = useStyles2(getStyles(vizHeight)); - const [fieldConfig, setFieldConfig] = useState(defaultFieldConfig(thresholds, data)); + const [fieldConfig, setFieldConfig] = useState(defaultFieldConfig(data, thresholds)); useEffect(() => { setFieldConfig((fieldConfig) => ({ @@ -109,7 +109,7 @@ function defaultUnit(data: PanelData): string | undefined { return data.series[0]?.fields.find((field) => field.type === 'number')?.config.unit; } -function defaultFieldConfig(thresholds: ThresholdsConfig, data: PanelData): PanelFieldConfig { +function defaultFieldConfig(data: PanelData, thresholds?: ThresholdsConfig): PanelFieldConfig { if (!thresholds) { return { defaults: {}, overrides: [] }; } diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx index c743f699a71..45b5719eeba 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewerVisualization.tsx @@ -1,35 +1,50 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React, { useCallback, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { DataSourceInstanceSettings, DateTime, dateTime, GrafanaTheme2, PanelData, urlUtil } from '@grafana/data'; +import { + DataSourceInstanceSettings, + DateTime, + dateTime, + GrafanaTheme2, + PanelData, + RelativeTimeRange, + urlUtil, +} from '@grafana/data'; import { config, getDataSourceSrv, PanelRenderer } from '@grafana/runtime'; import { Alert, CodeEditor, DateTimePicker, LinkButton, useStyles2, useTheme2 } from '@grafana/ui'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { PanelOptions } from 'app/plugins/panel/table/models.gen'; import { AccessControlAction } from 'app/types'; -import { AlertQuery } from 'app/types/unified-alerting-dto'; +import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { TABLE, TIMESERIES } from '../../utils/constants'; import { Authorize } from '../Authorize'; import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup'; -type RuleViewerVisualizationProps = { +interface RuleViewerVisualizationProps + extends Pick { data?: PanelData; - query: AlertQuery; - onChangeQuery: (query: AlertQuery) => void; -}; + onTimeRangeChange: (range: RelativeTimeRange) => void; + className?: string; +} const headerHeight = 4; -export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JSX.Element | null { +export function RuleViewerVisualization({ + data, + refId, + model, + datasourceUid, + relativeTimeRange, + onTimeRangeChange, + className, +}: RuleViewerVisualizationProps): JSX.Element | null { const theme = useTheme2(); const styles = useStyles2(getStyles); - const { data, query, onChangeQuery } = props; - const defaultPanel = isExpressionQuery(query.model) ? TABLE : TIMESERIES; + const defaultPanel = isExpressionQuery(model) ? TABLE : TIMESERIES; const [panel, setPanel] = useState(defaultPanel); - const dsSettings = getDataSourceSrv().getInstanceSettings(query.datasourceUid); - const relativeTimeRange = query.relativeTimeRange; + const dsSettings = getDataSourceSrv().getInstanceSettings(datasourceUid); const [options, setOptions] = useState({ frameIndex: 0, showHeader: true, @@ -41,13 +56,10 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS if (relativeTimeRange) { const interval = relativeTimeRange.from - relativeTimeRange.to; - onChangeQuery({ - ...query, - relativeTimeRange: { from: now + interval, to: now }, - }); + onTimeRangeChange({ from: now + interval, to: now }); } }, - [onChangeQuery, query, relativeTimeRange] + [onTimeRangeChange, relativeTimeRange] ); const setDateTime = useCallback((relativeTimeRangeTo: number) => { @@ -60,7 +72,7 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS if (!dsSettings) { return ( -
+
@@ -76,18 +88,14 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS } return ( -
+
{({ width, height }) => { return (
-
- {`Query ${query.refId}`} - ({dsSettings.name}) -
- {!isExpressionQuery(query.model) && relativeTimeRange ? ( + {!isExpressionQuery(model) && relativeTimeRange ? ( - {!isExpressionQuery(query.model) && ( + {!isExpressionQuery(model) && ( <>
View in Explore @@ -130,9 +138,9 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS ); } -function createExploreLink(settings: DataSourceInstanceSettings, query: AlertQuery): string { +function createExploreLink(settings: DataSourceInstanceSettings, model: AlertDataQuery): string { const { name } = settings; - const { refId, ...rest } = query.model; + const { refId, ...rest } = model; const queryParams = { ...rest, datasource: name }; return urlUtil.renderUrl(`${config.appSubUrl}/explore`, { @@ -154,7 +162,7 @@ const getStyles = (theme: GrafanaTheme2) => { height: ${theme.spacing(headerHeight)}; display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-end; white-space: nowrap; `, refId: css` diff --git a/public/app/features/expressions/components/Reduce.tsx b/public/app/features/expressions/components/Reduce.tsx index 1a1a898e8c7..58eda776162 100644 --- a/public/app/features/expressions/components/Reduce.tsx +++ b/public/app/features/expressions/components/Reduce.tsx @@ -3,7 +3,7 @@ import React, { FC } from 'react'; import { SelectableValue } from '@grafana/data'; import { InlineField, InlineFieldRow, Input, Select } from '@grafana/ui'; -import { ExpressionQuery, ExpressionQuerySettings, ReducerMode, reducerMode, reducerTypes } from '../types'; +import { ExpressionQuery, ExpressionQuerySettings, ReducerMode, reducerModes, reducerTypes } from '../types'; interface Props { labelWidth?: number | 'auto'; @@ -78,7 +78,7 @@ export const Reduce: FC = ({ labelWidth = 'auto', onChange, refIds, query - {replaceWithNumber()} diff --git a/public/app/features/expressions/types.ts b/public/app/features/expressions/types.ts index b67b9fe6a81..f48a7986c87 100644 --- a/public/app/features/expressions/types.ts +++ b/public/app/features/expressions/types.ts @@ -56,7 +56,7 @@ export enum ReducerMode { DropNonNumbers = 'dropNN', } -export const reducerMode: Array> = [ +export const reducerModes: Array> = [ { value: ReducerMode.Strict, label: 'Strict',