import { css, cx } from '@emotion/css'; import { capitalize, 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 { 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 { AlertQuery, PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { usePagination } from '../../hooks/usePagination'; import { HoverCard } from '../HoverCard'; import { Spacer } from '../Spacer'; import { AlertStateTag } from '../rules/AlertStateTag'; import { AlertConditionIndicator } from './AlertConditionIndicator'; import { formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries } from './util'; interface ExpressionProps { isAlertCondition?: boolean; data?: PanelData; error?: Error; warning?: Error; queries: AlertQuery[]; query: ExpressionQuery; onSetCondition: (refId: string) => void; onUpdateRefId: (oldRefId: string, newRefId: string) => void; onRemoveExpression: (refId: string) => void; onUpdateExpressionType: (refId: string, type: ExpressionQueryType) => void; onChangeQuery: (query: ExpressionQuery) => void; } export const Expression: FC = ({ queries = [], query, data, error, warning, isAlertCondition, onSetCondition, onUpdateRefId, onRemoveExpression, onUpdateExpressionType, onChangeQuery, }) => { const styles = useStyles2(getStyles); const queryType = query?.type; 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 alertCondition = isAlertCondition ?? false; const showSummary = isAlertCondition && hasResults; const groupedByState = { [PromAlertingRuleState.Firing]: series.filter((serie) => getSeriesValue(serie) >= 1), [PromAlertingRuleState.Inactive]: series.filter((serie) => getSeriesValue(serie) < 1), }; const renderExpressionType = useCallback( (query: ExpressionQuery) => { // these are the refs we can choose from that don't include the current one const availableRefIds = queries .filter((q) => query.refId !== q.refId) .map((q) => ({ value: q.refId, label: q.refId })); switch (query.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: {query.type}; } }, [onChangeQuery, queries] ); return (
onRemoveExpression(query.refId)} onUpdateRefId={(newRefId) => onUpdateRefId(query.refId, newRefId)} onUpdateExpressionType={(type) => onUpdateExpressionType(query.refId, type)} />
{renderExpressionType(query)}
{hasResults && }
onSetCondition(query.refId)} enabled={alertCondition} error={error} warning={warning} /> {showSummary && ( )}
); }; interface ExpressionResultProps { series: DataFrame[]; isAlertCondition?: boolean; } export const PAGE_SIZE = 20; export const ExpressionResult: FC = ({ series, isAlertCondition }) => { const { pageItems, previousPage, nextPage, numberOfPages, pageStart, pageEnd } = usePagination(series, 1, PAGE_SIZE); const styles = useStyles2(getStyles); // sometimes we receive results where every value is just "null" when noData occurs const emptyResults = isEmptySeries(series); const isTimeSeriesResults = !emptyResults && isTimeSeriesFrames(series); const shouldShowPagination = numberOfPages > 1; return (
{!emptyResults && isTimeSeriesResults && (
{pageItems.map((frame, index) => ( ))}
)} {!emptyResults && !isTimeSeriesResults && pageItems.map((frame, index) => ( // There's no way to uniquely identify a frame that doesn't cause render bugs :/ (Gilles) ))} {emptyResults &&
No data
} {shouldShowPagination && (
)}
); }; export const PreviewSummary: FC<{ firing: number; normal: number }> = ({ firing, normal }) => { const { mutedText } = useStyles2(getStyles); return {`${firing} firing, ${normal} normal`}; }; interface HeaderProps { refId: string; queryType: ExpressionQueryType; onUpdateRefId: (refId: string) => void; onRemoveExpression: () => void; onUpdateExpressionType: (type: ExpressionQueryType) => void; } const Header: FC = ({ refId, queryType, onUpdateRefId, onUpdateExpressionType, onRemoveExpression }) => { const styles = useStyles2(getStyles); const clearButton = useStyles2(clearButtonStyles); /** * There are 3 edit modes: * * 1. "refId": Editing the refId (ie. A -> B) * 2. "expressionType": Editing the type of the expression (ie. Reduce -> Math) * 3. "false": This means we're not editing either of those */ const [editMode, setEditMode] = useState<'refId' | 'expressionType' | false>(false); const editing = editMode !== false; const editingRefId = editing && editMode === 'refId'; const editingType = editing && editMode === 'expressionType'; const selectedExpressionType = gelTypes.find((o) => o.value === queryType); return (
{!editingRefId && ( )} {editingRefId && ( { onUpdateRefId(event.currentTarget.value); setEditMode(false); }} onFocus={(event) => event.target.select()} onBlur={(event) => { onUpdateRefId(event.currentTarget.value); setEditMode(false); }} /> )} {!editingType && ( )} {editingType && (