mirror of https://github.com/grafana/grafana.git
				
				
				
			Alerting: Create alert link from dashboard alerting panel (#63648)
* WIP * feat: update CSS for long names also adds broken href, to fix later * Create correct link using CombinedRules * Use link instead of button for alert link * Updates from PR review * Handle loading,haveResults and dispatched state for both promRules and rulerRules --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
		
							parent
							
								
									eca0a9f487
								
							
						
					
					
						commit
						a41e9b2dc7
					
				| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { createAsyncThunk, AsyncThunk } from '@reduxjs/toolkit';
 | 
			
		||||
import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit';
 | 
			
		||||
import { isEmpty } from 'lodash';
 | 
			
		||||
 | 
			
		||||
import { locationService, config } from '@grafana/runtime';
 | 
			
		||||
import { config, locationService } from '@grafana/runtime';
 | 
			
		||||
import {
 | 
			
		||||
  AlertmanagerAlert,
 | 
			
		||||
  AlertManagerCortexConfig,
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ import {
 | 
			
		|||
 | 
			
		||||
import { contextSrv } from '../../../../core/core';
 | 
			
		||||
import { backendSrv } from '../../../../core/services/backend_srv';
 | 
			
		||||
import { logInfo, LogMessages, withPerformanceLogging, trackNewAlerRuleFormSaved } from '../Analytics';
 | 
			
		||||
import { logInfo, LogMessages, trackNewAlerRuleFormSaved, withPerformanceLogging } from '../Analytics';
 | 
			
		||||
import {
 | 
			
		||||
  addAlertManagers,
 | 
			
		||||
  createOrUpdateSilence,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import {
 | 
			
		|||
  AlertingRule,
 | 
			
		||||
  CloudRuleIdentifier,
 | 
			
		||||
  CombinedRuleGroup,
 | 
			
		||||
  CombinedRuleWithLocation,
 | 
			
		||||
  GrafanaRuleIdentifier,
 | 
			
		||||
  PrometheusRuleIdentifier,
 | 
			
		||||
  PromRuleWithLocation,
 | 
			
		||||
| 
						 | 
				
			
			@ -26,10 +27,12 @@ import {
 | 
			
		|||
  RulerRuleDTO,
 | 
			
		||||
} from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import { CombinedRuleNamespace } from '../../../../types/unified-alerting';
 | 
			
		||||
import { State } from '../components/StateTag';
 | 
			
		||||
import { RuleHealth } from '../search/rulesSearchParser';
 | 
			
		||||
 | 
			
		||||
import { RULER_NOT_SUPPORTED_MSG } from './constants';
 | 
			
		||||
import { getRulesSourceName } from './datasource';
 | 
			
		||||
import { AsyncRequestState } from './redux';
 | 
			
		||||
 | 
			
		||||
export function isAlertingRule(rule: Rule | undefined): rule is AlertingRule {
 | 
			
		||||
| 
						 | 
				
			
			@ -112,6 +115,22 @@ export const flattenRules = (rules: RuleNamespace[]) => {
 | 
			
		|||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getAlertingRule = (rule: CombinedRuleWithLocation) =>
 | 
			
		||||
  isAlertingRule(rule.promRule) ? rule.promRule : null;
 | 
			
		||||
 | 
			
		||||
export const flattenCombinedRules = (rules: CombinedRuleNamespace[]) => {
 | 
			
		||||
  return rules.reduce<CombinedRuleWithLocation[]>((acc, { rulesSource, name: namespaceName, groups }) => {
 | 
			
		||||
    groups.forEach(({ name: groupName, rules }) => {
 | 
			
		||||
      rules.forEach((rule) => {
 | 
			
		||||
        if (rule.promRule && isAlertingRule(rule.promRule)) {
 | 
			
		||||
          acc.push({ dataSourceName: getRulesSourceName(rulesSource), namespaceName, groupName, ...rule });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return acc;
 | 
			
		||||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function alertStateToState(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): State {
 | 
			
		||||
  let key: PromAlertingRuleState | GrafanaAlertState | AlertState;
 | 
			
		||||
  if (Object.values(AlertState).includes(state as AlertState)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,8 +159,8 @@ const alertStateToStateMap: Record<PromAlertingRuleState | GrafanaAlertState | A
 | 
			
		|||
  [AlertState.Unknown]: 'info',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function getFirstActiveAt(promRule: AlertingRule) {
 | 
			
		||||
  if (!promRule.alerts) {
 | 
			
		||||
export function getFirstActiveAt(promRule?: AlertingRule) {
 | 
			
		||||
  if (!promRule?.alerts) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  return promRule.alerts.reduce((prev, alert) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,8 +18,9 @@ import {
 | 
			
		|||
import { config } from 'app/core/config';
 | 
			
		||||
import { contextSrv } from 'app/core/services/context_srv';
 | 
			
		||||
import alertDef from 'app/features/alerting/state/alertDef';
 | 
			
		||||
import { useCombinedRuleNamespaces } from 'app/features/alerting/unified/hooks/useCombinedRuleNamespaces';
 | 
			
		||||
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
 | 
			
		||||
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
 | 
			
		||||
import { fetchAllPromAndRulerRulesAction } from 'app/features/alerting/unified/state/actions';
 | 
			
		||||
import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
 | 
			
		||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -27,13 +28,16 @@ import {
 | 
			
		|||
  GRAFANA_DATASOURCE_NAME,
 | 
			
		||||
  GRAFANA_RULES_SOURCE_NAME,
 | 
			
		||||
} from 'app/features/alerting/unified/utils/datasource';
 | 
			
		||||
import { flattenRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
 | 
			
		||||
import { initialAsyncRequestState } from 'app/features/alerting/unified/utils/redux';
 | 
			
		||||
import { flattenCombinedRules, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
 | 
			
		||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
 | 
			
		||||
import { DashboardModel } from 'app/features/dashboard/state';
 | 
			
		||||
import { useDispatch, AccessControlAction } from 'app/types';
 | 
			
		||||
import { PromRuleWithLocation } from 'app/types/unified-alerting';
 | 
			
		||||
import { AccessControlAction, useDispatch } from 'app/types';
 | 
			
		||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import { getAlertingRule } from '../../../features/alerting/unified/utils/rules';
 | 
			
		||||
import { AlertingRule, CombinedRuleWithLocation } from '../../../types/unified-alerting';
 | 
			
		||||
 | 
			
		||||
import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
 | 
			
		||||
import GroupedModeView from './unified-alerting/GroupedView';
 | 
			
		||||
import UngroupedModeView from './unified-alerting/UngroupedView';
 | 
			
		||||
| 
						 | 
				
			
			@ -58,33 +62,38 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
 | 
			
		|||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    dispatch(fetchAllPromRulesAction());
 | 
			
		||||
    const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => dispatch(fetchAllPromRulesAction()));
 | 
			
		||||
    //we need promRules and rulerRules for getting the uid when creating the alert link in panel in case of being a rulerRule.
 | 
			
		||||
    dispatch(fetchAllPromAndRulerRulesAction());
 | 
			
		||||
    const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => dispatch(fetchAllPromAndRulerRulesAction()));
 | 
			
		||||
    return () => {
 | 
			
		||||
      sub?.unsubscribe();
 | 
			
		||||
    };
 | 
			
		||||
  }, [dispatch, dashboard]);
 | 
			
		||||
 | 
			
		||||
  const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
 | 
			
		||||
  const { prom, ruler } = useUnifiedAlertingSelector((state) => ({
 | 
			
		||||
    prom: state.promRules[GRAFANA_RULES_SOURCE_NAME] || initialAsyncRequestState,
 | 
			
		||||
    ruler: state.rulerRules[GRAFANA_RULES_SOURCE_NAME] || initialAsyncRequestState,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  const dispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched);
 | 
			
		||||
  const loading = rulesDataSourceNames.some((name) => promRulesRequests[name]?.loading);
 | 
			
		||||
  const haveResults = rulesDataSourceNames.some(
 | 
			
		||||
    (name) => promRulesRequests[name]?.result?.length && !promRulesRequests[name]?.error
 | 
			
		||||
  );
 | 
			
		||||
  const loading = prom.loading || ruler.loading;
 | 
			
		||||
  const haveResults = !!prom.result || !!ruler.result;
 | 
			
		||||
 | 
			
		||||
  const promRulesRequests = useUnifiedAlertingSelector((state) => state.promRules);
 | 
			
		||||
  const rulerRulesRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
 | 
			
		||||
  const combinedRules = useCombinedRuleNamespaces();
 | 
			
		||||
 | 
			
		||||
  const somePromRulesDispatched = rulesDataSourceNames.some((name) => promRulesRequests[name]?.dispatched);
 | 
			
		||||
  const someRulerRulesDispatched = rulesDataSourceNames.some((name) => rulerRulesRequests[name]?.dispatched);
 | 
			
		||||
  const dispatched = somePromRulesDispatched || someRulerRulesDispatched;
 | 
			
		||||
 | 
			
		||||
  const styles = useStyles2(getStyles);
 | 
			
		||||
 | 
			
		||||
  const flattenedCombinedRules = flattenCombinedRules(combinedRules);
 | 
			
		||||
  const order = props.options.sortOrder;
 | 
			
		||||
 | 
			
		||||
  const rules = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      filterRules(
 | 
			
		||||
        props,
 | 
			
		||||
        sortRules(
 | 
			
		||||
          props.options.sortOrder,
 | 
			
		||||
          Object.values(promRulesRequests).flatMap(({ result = [] }) => flattenRules(result))
 | 
			
		||||
        )
 | 
			
		||||
      ),
 | 
			
		||||
    [props, promRulesRequests]
 | 
			
		||||
    () => filterRules(props, sortRules(order, flattenedCombinedRules)),
 | 
			
		||||
    [flattenedCombinedRules, order, props]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const noAlertsMessage = rules.length === 0 ? 'No alerts matching filters' : undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -127,16 +136,24 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sortRules(sortOrder: SortOrder, rules: PromRuleWithLocation[]) {
 | 
			
		||||
function sortRules(sortOrder: SortOrder, rules: CombinedRuleWithLocation[]) {
 | 
			
		||||
  if (sortOrder === SortOrder.Importance) {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return sortBy(rules, (rule) => alertDef.alertStateSortScore[rule.state]);
 | 
			
		||||
  } else if (sortOrder === SortOrder.TimeAsc) {
 | 
			
		||||
    return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date());
 | 
			
		||||
    return sortBy(rules, (rule) => {
 | 
			
		||||
      //at this point rules are all AlertingRule, this check is only needed for Typescript checks
 | 
			
		||||
      const alertingRule: AlertingRule | undefined = getAlertingRule(rule) ?? undefined;
 | 
			
		||||
      return getFirstActiveAt(alertingRule) || new Date();
 | 
			
		||||
    });
 | 
			
		||||
  } else if (sortOrder === SortOrder.TimeDesc) {
 | 
			
		||||
    return sortBy(rules, (rule) => getFirstActiveAt(rule.rule) || new Date()).reverse();
 | 
			
		||||
    return sortBy(rules, (rule) => {
 | 
			
		||||
      //at this point rules are all AlertingRule, this check is only needed for Typescript checks
 | 
			
		||||
      const alertingRule: AlertingRule | undefined = getAlertingRule(rule) ?? undefined;
 | 
			
		||||
      return getFirstActiveAt(alertingRule) || new Date();
 | 
			
		||||
    }).reverse();
 | 
			
		||||
  }
 | 
			
		||||
  const result = sortBy(rules, (rule) => rule.rule.name.toLowerCase());
 | 
			
		||||
  const result = sortBy(rules, (rule) => rule.name.toLowerCase());
 | 
			
		||||
  if (sortOrder === SortOrder.AlphaDesc) {
 | 
			
		||||
    result.reverse();
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -144,39 +161,46 @@ function sortRules(sortOrder: SortOrder, rules: PromRuleWithLocation[]) {
 | 
			
		|||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: PromRuleWithLocation[]) {
 | 
			
		||||
function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: CombinedRuleWithLocation[]) {
 | 
			
		||||
  const { options, replaceVariables } = props;
 | 
			
		||||
 | 
			
		||||
  let filteredRules = [...rules];
 | 
			
		||||
  if (options.dashboardAlerts) {
 | 
			
		||||
    const dashboardUid = getDashboardSrv().getCurrent()?.uid;
 | 
			
		||||
    filteredRules = filteredRules.filter(({ rule: { annotations = {} } }) =>
 | 
			
		||||
    filteredRules = filteredRules.filter(({ annotations = {} }) =>
 | 
			
		||||
      Object.entries(annotations).some(([key, value]) => key === Annotation.dashboardUID && value === dashboardUid)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (options.alertName) {
 | 
			
		||||
    const replacedName = replaceVariables(options.alertName);
 | 
			
		||||
    filteredRules = filteredRules.filter(({ rule: { name } }) =>
 | 
			
		||||
    filteredRules = filteredRules.filter(({ name }) =>
 | 
			
		||||
      name.toLocaleLowerCase().includes(replacedName.toLocaleLowerCase())
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  filteredRules = filteredRules.filter((rule) => {
 | 
			
		||||
    const alertingRule = getAlertingRule(rule);
 | 
			
		||||
    if (!alertingRule) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      (options.stateFilter.firing && rule.rule.state === PromAlertingRuleState.Firing) ||
 | 
			
		||||
      (options.stateFilter.pending && rule.rule.state === PromAlertingRuleState.Pending) ||
 | 
			
		||||
      (options.stateFilter.normal && rule.rule.state === PromAlertingRuleState.Inactive)
 | 
			
		||||
      (options.stateFilter.firing && alertingRule.state === PromAlertingRuleState.Firing) ||
 | 
			
		||||
      (options.stateFilter.pending && alertingRule.state === PromAlertingRuleState.Pending) ||
 | 
			
		||||
      (options.stateFilter.normal && alertingRule.state === PromAlertingRuleState.Inactive)
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (options.alertInstanceLabelFilter) {
 | 
			
		||||
    const replacedLabelFilter = replaceVariables(options.alertInstanceLabelFilter);
 | 
			
		||||
    const matchers = parseMatchers(replacedLabelFilter);
 | 
			
		||||
 | 
			
		||||
    // Reduce rules and instances to only those that match
 | 
			
		||||
    filteredRules = filteredRules.reduce<PromRuleWithLocation[]>((rules, rule) => {
 | 
			
		||||
      const filteredAlerts = (rule.rule.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers));
 | 
			
		||||
    filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => {
 | 
			
		||||
      const alertingRule = getAlertingRule(rule);
 | 
			
		||||
      const filteredAlerts = (alertingRule?.alerts ?? []).filter(({ labels }) => labelsMatchMatchers(labels, matchers));
 | 
			
		||||
      if (filteredAlerts.length) {
 | 
			
		||||
        rules.push({ ...rule, rule: { ...rule.rule, alerts: filteredAlerts } });
 | 
			
		||||
        const alertRule: AlertingRule | null = getAlertingRule(rule);
 | 
			
		||||
        alertRule && rules.push({ ...rule, promRule: { ...alertRule, alerts: filteredAlerts } });
 | 
			
		||||
      }
 | 
			
		||||
      return rules;
 | 
			
		||||
    }, []);
 | 
			
		||||
| 
						 | 
				
			
			@ -200,8 +224,9 @@ function filterRules(props: PanelProps<UnifiedAlertListOptions>, rules: PromRule
 | 
			
		|||
  // Remove rules having 0 instances
 | 
			
		||||
  // AlertInstances filters instances and we need to prevent situation
 | 
			
		||||
  // when we display a rule with 0 instances
 | 
			
		||||
  filteredRules = filteredRules.reduce<PromRuleWithLocation[]>((rules, rule) => {
 | 
			
		||||
    const filteredAlerts = filterAlerts(options, rule.rule.alerts ?? []);
 | 
			
		||||
  filteredRules = filteredRules.reduce<CombinedRuleWithLocation[]>((rules, rule) => {
 | 
			
		||||
    const alertingRule = getAlertingRule(rule);
 | 
			
		||||
    const filteredAlerts = alertingRule ? filterAlerts(options, alertingRule.alerts ?? []) : [];
 | 
			
		||||
    if (filteredAlerts.length) {
 | 
			
		||||
      // We intentionally don't set alerts to filteredAlerts
 | 
			
		||||
      // because later we couldn't display that some alerts are hidden (ref AlertInstances filtering)
 | 
			
		||||
| 
						 | 
				
			
			@ -239,13 +264,23 @@ export const getStyles = (theme: GrafanaTheme2) => ({
 | 
			
		|||
    border-radius: ${theme.shape.borderRadius(2)};
 | 
			
		||||
    margin-bottom: ${theme.spacing(0.5)};
 | 
			
		||||
 | 
			
		||||
    & > * {
 | 
			
		||||
      margin-right: ${theme.spacing(1)};
 | 
			
		||||
    }
 | 
			
		||||
    gap: ${theme.spacing(2)};
 | 
			
		||||
  `,
 | 
			
		||||
  alertName: css`
 | 
			
		||||
    font-size: ${theme.typography.h6.fontSize};
 | 
			
		||||
    font-weight: ${theme.typography.fontWeightBold};
 | 
			
		||||
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
  `,
 | 
			
		||||
  alertNameWrapper: css`
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    flex-wrap: nowrap;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
 | 
			
		||||
    min-width: 100px;
 | 
			
		||||
  `,
 | 
			
		||||
  alertLabels: css`
 | 
			
		||||
    > * {
 | 
			
		||||
| 
						 | 
				
			
			@ -290,4 +325,8 @@ export const getStyles = (theme: GrafanaTheme2) => ({
 | 
			
		|||
  customGroupDetails: css`
 | 
			
		||||
    margin-bottom: ${theme.spacing(0.5)};
 | 
			
		||||
  `,
 | 
			
		||||
  link: css`
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
    color: ${theme.colors.primary.text};
 | 
			
		||||
  `,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,15 +2,17 @@ import React, { FC, useMemo } from 'react';
 | 
			
		|||
 | 
			
		||||
import { useStyles2 } from '@grafana/ui';
 | 
			
		||||
import { AlertLabel } from 'app/features/alerting/unified/components/AlertLabel';
 | 
			
		||||
import { Alert, PromRuleWithLocation } from 'app/types/unified-alerting';
 | 
			
		||||
import { getAlertingRule } from 'app/features/alerting/unified/utils/rules';
 | 
			
		||||
import { Alert } from 'app/types/unified-alerting';
 | 
			
		||||
 | 
			
		||||
import { AlertingRule, CombinedRuleWithLocation } from '../../../../types/unified-alerting';
 | 
			
		||||
import { AlertInstances } from '../AlertInstances';
 | 
			
		||||
import { getStyles } from '../UnifiedAlertList';
 | 
			
		||||
import { GroupedRules, UnifiedAlertListOptions } from '../types';
 | 
			
		||||
import { filterAlerts } from '../util';
 | 
			
		||||
 | 
			
		||||
type GroupedModeProps = {
 | 
			
		||||
  rules: PromRuleWithLocation[];
 | 
			
		||||
  rules: CombinedRuleWithLocation[];
 | 
			
		||||
  options: UnifiedAlertListOptions;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -22,12 +24,13 @@ const GroupedModeView: FC<GroupedModeProps> = ({ rules, options }) => {
 | 
			
		|||
  const groupedRules = useMemo<GroupedRules>(() => {
 | 
			
		||||
    const groupedRules = new Map<string, Alert[]>();
 | 
			
		||||
 | 
			
		||||
    const hasInstancesWithMatchingLabels = (rule: PromRuleWithLocation) =>
 | 
			
		||||
      groupBy ? alertHasEveryLabel(rule, groupBy) : true;
 | 
			
		||||
    const hasInstancesWithMatchingLabels = (rule: CombinedRuleWithLocation) =>
 | 
			
		||||
      groupBy ? alertHasEveryLabelForCombinedRules(rule, groupBy) : true;
 | 
			
		||||
 | 
			
		||||
    const matchingRules = rules.filter(hasInstancesWithMatchingLabels);
 | 
			
		||||
    matchingRules.forEach((rule: PromRuleWithLocation) => {
 | 
			
		||||
      (rule.rule.alerts ?? []).forEach((alert) => {
 | 
			
		||||
    matchingRules.forEach((rule: CombinedRuleWithLocation) => {
 | 
			
		||||
      const alertingRule: AlertingRule | null = getAlertingRule(rule);
 | 
			
		||||
      (alertingRule?.alerts ?? []).forEach((alert) => {
 | 
			
		||||
        const mapKey = createMapKey(groupBy, alert.labels);
 | 
			
		||||
        const existingAlerts = groupedRules.get(mapKey) ?? [];
 | 
			
		||||
        groupedRules.set(mapKey, [...existingAlerts, alert]);
 | 
			
		||||
| 
						 | 
				
			
			@ -75,9 +78,10 @@ function parseMapKey(key: string): Array<[string, string]> {
 | 
			
		|||
  return [...new URLSearchParams(key)];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function alertHasEveryLabel(rule: PromRuleWithLocation, groupByKeys: string[]) {
 | 
			
		||||
function alertHasEveryLabelForCombinedRules(rule: CombinedRuleWithLocation, groupByKeys: string[]) {
 | 
			
		||||
  const alertingRule: AlertingRule | null = getAlertingRule(rule);
 | 
			
		||||
  return groupByKeys.every((key) => {
 | 
			
		||||
    return (rule.rule.alerts ?? []).some((alert) => alert.labels[key]);
 | 
			
		||||
    return (alertingRule?.alerts ?? []).some((alert) => alert.labels[key]);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,25 +1,36 @@
 | 
			
		|||
import { css } from '@emotion/css';
 | 
			
		||||
import React, { FC } from 'react';
 | 
			
		||||
import { useLocation } from 'react-use';
 | 
			
		||||
 | 
			
		||||
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
 | 
			
		||||
import { Stack } from '@grafana/experimental';
 | 
			
		||||
import { Icon, useStyles2 } from '@grafana/ui';
 | 
			
		||||
import alertDef from 'app/features/alerting/state/alertDef';
 | 
			
		||||
import { alertStateToReadable, alertStateToState, getFirstActiveAt } from 'app/features/alerting/unified/utils/rules';
 | 
			
		||||
import { PromRuleWithLocation } from 'app/types/unified-alerting';
 | 
			
		||||
import { Spacer } from 'app/features/alerting/unified/components/Spacer';
 | 
			
		||||
import { fromCombinedRule, stringifyIdentifier } from 'app/features/alerting/unified/utils/rule-id';
 | 
			
		||||
import {
 | 
			
		||||
  alertStateToReadable,
 | 
			
		||||
  alertStateToState,
 | 
			
		||||
  getFirstActiveAt,
 | 
			
		||||
  isAlertingRule,
 | 
			
		||||
} from 'app/features/alerting/unified/utils/rules';
 | 
			
		||||
import { createUrl } from 'app/features/alerting/unified/utils/url';
 | 
			
		||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import { AlertingRule, CombinedRuleWithLocation } from '../../../../types/unified-alerting';
 | 
			
		||||
import { AlertInstances } from '../AlertInstances';
 | 
			
		||||
import { getStyles } from '../UnifiedAlertList';
 | 
			
		||||
import { UnifiedAlertListOptions } from '../types';
 | 
			
		||||
 | 
			
		||||
type UngroupedModeProps = {
 | 
			
		||||
  rules: PromRuleWithLocation[];
 | 
			
		||||
  rules: CombinedRuleWithLocation[];
 | 
			
		||||
  options: UnifiedAlertListOptions;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
 | 
			
		||||
  const styles = useStyles2(getStyles);
 | 
			
		||||
  const stateStyle = useStyles2(getStateTagStyles);
 | 
			
		||||
  const { href: returnTo } = useLocation();
 | 
			
		||||
 | 
			
		||||
  const rulesToDisplay = rules.length <= options.maxItems ? rules : rules.slice(0, options.maxItems);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,27 +38,52 @@ const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
 | 
			
		|||
    <>
 | 
			
		||||
      <ol className={styles.alertRuleList}>
 | 
			
		||||
        {rulesToDisplay.map((ruleWithLocation, index) => {
 | 
			
		||||
          const { rule, namespaceName, groupName } = ruleWithLocation;
 | 
			
		||||
          const firstActiveAt = getFirstActiveAt(rule);
 | 
			
		||||
          const { namespaceName, groupName, dataSourceName } = ruleWithLocation;
 | 
			
		||||
          const alertingRule: AlertingRule | undefined = isAlertingRule(ruleWithLocation.promRule)
 | 
			
		||||
            ? ruleWithLocation.promRule
 | 
			
		||||
            : undefined;
 | 
			
		||||
          const firstActiveAt = getFirstActiveAt(alertingRule);
 | 
			
		||||
          const indentifier = fromCombinedRule(ruleWithLocation.dataSourceName, ruleWithLocation);
 | 
			
		||||
          const strIndentifier = stringifyIdentifier(indentifier);
 | 
			
		||||
 | 
			
		||||
          const href = createUrl(
 | 
			
		||||
            `/alerting/${encodeURIComponent(dataSourceName)}/${encodeURIComponent(strIndentifier)}/view`,
 | 
			
		||||
            { returnTo: returnTo ?? '' }
 | 
			
		||||
          );
 | 
			
		||||
          if (alertingRule) {
 | 
			
		||||
            return (
 | 
			
		||||
            <li className={styles.alertRuleItem} key={`alert-${namespaceName}-${groupName}-${rule.name}-${index}`}>
 | 
			
		||||
              <li
 | 
			
		||||
                className={styles.alertRuleItem}
 | 
			
		||||
                key={`alert-${namespaceName}-${groupName}-${ruleWithLocation.name}-${index}`}
 | 
			
		||||
              >
 | 
			
		||||
                <div className={stateStyle.icon}>
 | 
			
		||||
                  <Icon
 | 
			
		||||
                  name={alertDef.getStateDisplayModel(rule.state).iconClass}
 | 
			
		||||
                  className={stateStyle[alertStateToState(rule.state)]}
 | 
			
		||||
                    name={alertDef.getStateDisplayModel(alertingRule.state).iconClass}
 | 
			
		||||
                    className={stateStyle[alertStateToState(alertingRule.state)]}
 | 
			
		||||
                    size={'lg'}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              <div>
 | 
			
		||||
                <div className={styles.alertNameWrapper}>
 | 
			
		||||
                  <div className={styles.instanceDetails}>
 | 
			
		||||
                  <div className={styles.alertName} title={rule.name}>
 | 
			
		||||
                    {rule.name}
 | 
			
		||||
                    <Stack direction="row" gap={1} wrap={false}>
 | 
			
		||||
                      <div className={styles.alertName} title={ruleWithLocation.name}>
 | 
			
		||||
                        {ruleWithLocation.name}
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <Spacer />
 | 
			
		||||
                      {href && (
 | 
			
		||||
                        <a href={href} target="__blank" className={styles.link} rel="noopener">
 | 
			
		||||
                          <Stack alignItems="center" gap={1}>
 | 
			
		||||
                            View alert rule
 | 
			
		||||
                            <Icon name={'external-link-alt'} size="sm" />
 | 
			
		||||
                          </Stack>
 | 
			
		||||
                        </a>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </Stack>
 | 
			
		||||
                    <div className={styles.alertDuration}>
 | 
			
		||||
                    <span className={stateStyle[alertStateToState(rule.state)]}>
 | 
			
		||||
                      {alertStateToReadable(rule.state)}
 | 
			
		||||
                      <span className={stateStyle[alertStateToState(alertingRule.state)]}>
 | 
			
		||||
                        {alertStateToReadable(alertingRule.state)}
 | 
			
		||||
                      </span>{' '}
 | 
			
		||||
                    {firstActiveAt && rule.state !== PromAlertingRuleState.Inactive && (
 | 
			
		||||
                      {firstActiveAt && alertingRule.state !== PromAlertingRuleState.Inactive && (
 | 
			
		||||
                        <>
 | 
			
		||||
                          for{' '}
 | 
			
		||||
                          <span>
 | 
			
		||||
| 
						 | 
				
			
			@ -60,10 +96,13 @@ const UngroupedModeView: FC<UngroupedModeProps> = ({ rules, options }) => {
 | 
			
		|||
                      )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                <AlertInstances alerts={ruleWithLocation.rule.alerts ?? []} options={options} />
 | 
			
		||||
                  <AlertInstances alerts={alertingRule.alerts ?? []} options={options} />
 | 
			
		||||
                </div>
 | 
			
		||||
              </li>
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            return null;
 | 
			
		||||
          }
 | 
			
		||||
        })}
 | 
			
		||||
      </ol>
 | 
			
		||||
    </>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,15 +3,15 @@
 | 
			
		|||
import { AlertState, DataSourceInstanceSettings } from '@grafana/data';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Annotations,
 | 
			
		||||
  GrafanaAlertState,
 | 
			
		||||
  GrafanaAlertStateWithReason,
 | 
			
		||||
  Labels,
 | 
			
		||||
  mapStateWithReasonToBaseState,
 | 
			
		||||
  PromAlertingRuleState,
 | 
			
		||||
  PromRuleType,
 | 
			
		||||
  RulerRuleDTO,
 | 
			
		||||
  Labels,
 | 
			
		||||
  Annotations,
 | 
			
		||||
  RulerRuleGroupDTO,
 | 
			
		||||
  GrafanaAlertState,
 | 
			
		||||
  GrafanaAlertStateWithReason,
 | 
			
		||||
  mapStateWithReasonToBaseState,
 | 
			
		||||
} from './unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
export type Alert = {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +111,12 @@ export interface RuleWithLocation<T = RulerRuleDTO> {
 | 
			
		|||
  rule: T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CombinedRuleWithLocation extends CombinedRule {
 | 
			
		||||
  dataSourceName: string;
 | 
			
		||||
  namespaceName: string;
 | 
			
		||||
  groupName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PromRuleWithLocation {
 | 
			
		||||
  rule: AlertingRule;
 | 
			
		||||
  dataSourceName: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue