Submit
Path:
~
/
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
alerting
/
unified
/
components
/
rule-editor
/
File Content:
util.ts
import { xor } from 'lodash'; import { ValidateResult } from 'react-hook-form'; import { DataFrame, isTimeSeriesFrames, LoadingState, PanelData, ThresholdsConfig, ThresholdsMode, } from '@grafana/data'; import { GraphThresholdsStyleMode } from '@grafana/schema'; import { config } from 'app/core/config'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { ClassicCondition, ExpressionQueryType } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { RuleFormType } from '../../types/rule-form'; import { createDagFromQueries, getOriginOfRefId } from './dag'; export function queriesWithUpdatedReferences( queries: AlertQuery[], previousRefId: string, newRefId: string ): AlertQuery[] { return queries.map((query) => { if (previousRefId === newRefId) { return query; } if (!isExpressionQuery(query.model)) { return query; } const isMathExpression = query.model.type === 'math'; const isReduceExpression = query.model.type === 'reduce'; const isResampleExpression = query.model.type === 'resample'; const isClassicExpression = query.model.type === 'classic_conditions'; const isThresholdExpression = query.model.type === 'threshold'; if (isMathExpression) { return { ...query, model: { ...query.model, expression: updateMathExpressionRefs(query.model.expression ?? '', previousRefId, newRefId), }, }; } if (isResampleExpression || isReduceExpression || isThresholdExpression) { const isReferencing = query.model.expression === previousRefId; return { ...query, model: { ...query.model, expression: isReferencing ? newRefId : query.model.expression, }, }; } if (isClassicExpression) { const conditions = query.model.conditions?.map((condition) => ({ ...condition, query: { ...condition.query, params: condition.query.params.map((param: string) => (param === previousRefId ? newRefId : param)), }, })); return { ...query, model: { ...query.model, conditions } }; } return query; }); } export function updateMathExpressionRefs(expression: string, previousRefId: string, newRefId: string): string { const oldExpression = new RegExp('(\\$' + previousRefId + '\\b)|(\\${' + previousRefId + '})', 'gm'); const newExpression = '${' + newRefId + '}'; return expression.replace(oldExpression, newExpression); } export function refIdExists(queries: AlertQuery[], refId: string | null): boolean { return queries.find((query) => query.refId === refId) !== undefined; } // some gateways (like Istio) will decode "/" and "\" characters – this will cause 404 errors for any API call // that includes these values in the URL (ie. /my/path%2fto/resource -> /my/path/to/resource) // // see https://istio.io/latest/docs/ops/best-practices/security/#customize-your-system-on-path-normalization export function checkForPathSeparator(value: string): ValidateResult { const containsPathSeparator = value.includes('/') || value.includes('\\'); if (containsPathSeparator) { return 'Cannot contain "/" or "\\" characters'; } return true; } // this function assumes we've already checked if the data passed in to the function is of the alert condition export function errorFromCurrentCondition(data: PanelData): Error | undefined { if (data.series.length === 0) { return; } const isTimeSeriesResults = isTimeSeriesFrames(data.series); let error; if (isTimeSeriesResults) { error = new Error('You cannot use time series data as an alert condition, consider adding a reduce expression.'); } return error; } export function errorFromPreviewData(data: PanelData): Error | undefined { // give preference to QueryErrors if (data.errors?.length) { return new Error(data.errors[0].message); } return; } export function warningFromSeries(series: DataFrame[]): Error | undefined { const notices = series[0]?.meta?.notices ?? []; const warning = notices.find((notice) => notice.severity === 'warning')?.text; return warning ? new Error(warning) : undefined; } export type ThresholdDefinition = { config: ThresholdsConfig; mode: GraphThresholdsStyleMode; }; export type ThresholdDefinitions = Record<string, ThresholdDefinition>; /** * This function will retrieve threshold definitions for the given array of data and expression queries. */ export function getThresholdsForQueries(queries: AlertQuery[], condition: string | null) { const thresholds: ThresholdDefinitions = {}; const SUPPORTED_EXPRESSION_TYPES = [ExpressionQueryType.threshold, ExpressionQueryType.classic]; if (!condition) { return thresholds; } for (const query of queries) { if (!isExpressionQuery(query.model)) { continue; } // currently only supporting "threshold" & "classic_condition" expressions if (!SUPPORTED_EXPRESSION_TYPES.includes(query.model.type)) { continue; } if (!Array.isArray(query.model.conditions)) { continue; } if (query.model.refId !== condition) { continue; } // if any of the conditions are a "range" we switch to an "area" threshold view and ignore single threshold values // the time series panel does not support both. const hasRangeThreshold = query.model.conditions.some(isRangeCondition); query.model.conditions.forEach((condition) => { const threshold = condition.evaluator.params; // "classic_conditions" use `condition.query.params[]` and "threshold" uses `query.model.expression` const refId = condition.query?.params[0] ?? query.model.expression; // if an expression hasn't been linked to a data query yet, it won't have a refId if (!refId) { return; } const isRangeThreshold = isRangeCondition(condition); try { // create a DAG so we can find the origin of the current expression const graph = createDagFromQueries(queries); const originRefIDs = getOriginOfRefId(refId, graph); const originQueries = queries.filter((query) => originRefIDs.includes(query.refId)); originQueries.forEach((originQuery) => { const originRefID = originQuery.refId; // check if the origin is a data query const originIsDataQuery = !isExpressionQuery(originQuery?.model); // if yes, add threshold config to the refId of the data Query const hasValidOrigin = Boolean(originIsDataQuery && originRefID); // create the initial data structure for this origin refId if (originRefID && !thresholds[originRefID]) { thresholds[originRefID] = { config: { mode: ThresholdsMode.Absolute, steps: [], }, mode: GraphThresholdsStyleMode.Line, }; } if (originRefID && hasValidOrigin && !isRangeThreshold && !hasRangeThreshold) { appendSingleThreshold(originRefID, threshold[0]); } else if (originRefID && hasValidOrigin && isRangeThreshold) { appendRangeThreshold(originRefID, threshold, condition.evaluator.type); thresholds[originRefID].mode = GraphThresholdsStyleMode.LineAndArea; } }); } catch (err) { console.error('Failed to parse thresholds', err); return; } }); } function appendSingleThreshold(refId: string, value: number): void { thresholds[refId].config.steps.push( ...[ { value: -Infinity, color: 'transparent', }, { value: value, color: config.theme2.colors.error.main, }, ] ); } function appendRangeThreshold(refId: string, values: number[], type: EvalFunction): void { if (type === EvalFunction.IsWithinRange) { thresholds[refId].config.steps.push( ...[ { value: -Infinity, color: 'transparent', }, { value: values[0], color: config.theme2.colors.error.main, }, { value: values[1], color: config.theme2.colors.error.main, }, { value: values[1], color: 'transparent', }, ] ); } if (type === EvalFunction.IsOutsideRange) { thresholds[refId].config.steps.push( ...[ { value: -Infinity, color: config.theme2.colors.error.main, }, // we have to duplicate this value, or the graph will not display the handle in the right color { value: values[0], color: config.theme2.colors.error.main, }, { value: values[0], color: 'transparent', }, { value: values[1], color: config.theme2.colors.error.main, }, ] ); } // now also sort the threshold values, if we don't then they will look weird in the time series panel // TODO this doesn't work for negative values for now, those need to be sorted inverse thresholds[refId].config.steps.sort((a, b) => a.value - b.value); // also make sure we remove any "undefined" values from our steps in case the threshold config is incomplete thresholds[refId].config.steps = thresholds[refId].config.steps.filter((step) => step.value !== undefined); } return thresholds; } function isRangeCondition(condition: ClassicCondition) { return ( condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange ); } export function getStatusMessage(data: PanelData): string | undefined { const genericErrorMessage = 'Failed to fetch data'; if (data.state !== LoadingState.Error) { return; } const errors = data.errors; if (errors?.length) { return errors.map((error) => error.message ?? genericErrorMessage).join(', '); } return data.error?.message ?? genericErrorMessage; } export function translateRouteParamToRuleType(param = ''): RuleFormType { if (param === 'recording') { return RuleFormType.cloudRecording; } return RuleFormType.grafana; } /** * This function finds what refIds have been updated given the previous Array of queries and an Array of updated data queries. * All expression queries are discarded from the arrays, since we have separate handlers for those (see "onUpdateRefId") of the ExpressionEditor * * This code assumes not more than 1 query refId has changed per "onChangeQueries", */ export function findRenamedDataQueryReferences( previousQueries: AlertQuery[], updatedQueries: AlertQuery[] ): [string, string] { const updatedDataQueries = updatedQueries .filter((query) => !isExpressionQuery(query.model)) .map((query) => query.refId); const previousDataQueries = previousQueries .filter((query) => !isExpressionQuery(query.model)) .map((query) => query.refId); // given the following two arrays // ['A', 'B', 'C'] and ['FOO', 'B' 'C'] // the "xor" function will return ['A', 'FOO'] because those are not in both arrays const [oldRefId, newRefId] = xor(previousDataQueries, updatedDataQueries); return [oldRefId, newRefId]; }
Submit
FILE
FOLDER
INFO
Name
Size
Permission
Action
__snapshots__
---
0755
alert-rule-form
---
0755
notificaton-preview
---
0755
query-and-alert-condition
---
0755
rule-types
---
0755
AlertRuleNameInput.tsx
1548 bytes
0644
AnnotationHeaderField.tsx
2118 bytes
0644
AnnotationKeyInput.tsx
1011 bytes
0644
AnnotationsStep.test.tsx
12955 bytes
0644
AnnotationsStep.tsx
9351 bytes
0644
CloudAlertPreview.tsx
2447 bytes
0644
CloudEvaluationBehavior.tsx
2339 bytes
0644
CloudRulesSourcePicker.tsx
1232 bytes
0644
CustomAnnotationHeaderField.tsx
978 bytes
0644
DashboardAnnotationField.tsx
2211 bytes
0644
DashboardPicker.test.tsx
2359 bytes
0644
DashboardPicker.tsx
11583 bytes
0644
ExpressionEditor.tsx
4464 bytes
0644
ExpressionsEditor.tsx
2456 bytes
0644
FolderAndGroup.tsx
15170 bytes
0644
GrafanaAlertStatePicker.tsx
1246 bytes
0644
GrafanaEvaluationBehavior.tsx
11886 bytes
0644
GrafanaRuleInspector.tsx
0 bytes
0644
GroupAndNamespaceFields.tsx
3633 bytes
0644
LabelsField.test.tsx
5489 bytes
0644
LabelsField.tsx
11099 bytes
0644
NeedHelpInfo.tsx
1656 bytes
0644
NotificationsStep.tsx
9351 bytes
0644
PreviewRule.tsx
3862 bytes
0644
PreviewRuleResult.tsx
2519 bytes
0644
QueryEditor.tsx
1317 bytes
0644
QueryOptions.tsx
2986 bytes
0644
QueryRows.tsx
10036 bytes
0644
QueryWrapper.tsx
8477 bytes
0644
RecordingRuleEditor.tsx
3160 bytes
0644
RecordingRulesNameSpaceAndGroupStep.tsx
743 bytes
0644
RuleEditorSection.tsx
1387 bytes
0644
RuleFolderPicker.tsx
2311 bytes
0644
RuleInspector.tsx
4989 bytes
0644
SelectWIthAdd.tsx
2207 bytes
0644
VizWrapper.tsx
2873 bytes
0644
dag.test.ts
3645 bytes
0644
dag.ts
3498 bytes
0644
preview.test.ts
5286 bytes
0644
preview.ts
1541 bytes
0644
useDashboardQuery.ts
1014 bytes
0644
util.test.ts
12574 bytes
0644
util.ts
11522 bytes
0644
N4ST4R_ID | Naxtarrr