mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Simplify routing in alert form - part1 (#78040)
* Add routing option tabs * Use alertingSimplifiedRouting feature toggle * Move simplified routing tab to a separate component:SimplifiedRouting * Populate contact point selector with the right values * Show alert manager icons * Fix descriptions * Remove clear button on ContactPointSelector and save updated reducer state in the form * Load contact points and manual option from rule data in RuleFormValues * make contact point selector not clearable * Refactor * Add link to contact points view * Move ContactPointSelector to a separate file * Refactor: move hoook useReceiversMetadataMapByName to a separate file * Update Need more info texts * Address some PR review comments * Use useContactPointsWithStatus hook and wrap each ContacPointSelector with AlertmanagerProvider * use getAlertManagerDataSourcesByPermission instead of useGetAlertManagersMetadata in NotificationPreview * Update enum * Remove css style * remove console * update contact point selector * file cleanup * adds summary as description * Update text in manual tab * Fix preview routing not checking if alert manager can handle grafana alerts * Fix typo * remove unused location form field * fix prettier * fix test * Remove unused location form field from AlertRuleNameInput * Only use internal AlertManager for now --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
parent
72759be6ec
commit
2acf153a26
@ -2266,16 +2266,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/rule-editor/LabelsField.tsx:5381": [
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "6"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "7"]
|
|
||||||
],
|
|
||||||
"public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx:5381": [
|
"public/app/features/alerting/unified/components/rule-editor/NeedHelpInfo.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||||
|
@ -10,7 +10,9 @@ const fetch = jest.fn();
|
|||||||
|
|
||||||
jest.mock('./prometheus');
|
jest.mock('./prometheus');
|
||||||
jest.mock('./ruler');
|
jest.mock('./ruler');
|
||||||
jest.mock('app/core/services/context_srv', () => {});
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
|
contextSrv: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getBackendSrv: () => ({ fetch }),
|
getBackendSrv: () => ({ fetch }),
|
||||||
|
@ -325,7 +325,7 @@ export const ContactPoint = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className={styles.integrationWrapper}>
|
||||||
<ContactPointReceiverSummary receivers={receivers} />
|
<ContactPointReceiverSummary receivers={receivers} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -493,12 +493,10 @@ type ContactPointReceiverSummaryProps = {
|
|||||||
* This summary is used when we're dealing with non-Grafana managed alertmanager since they
|
* This summary is used when we're dealing with non-Grafana managed alertmanager since they
|
||||||
* don't have any metadata worth showing other than a summary of what types are configured for the contact point
|
* don't have any metadata worth showing other than a summary of what types are configured for the contact point
|
||||||
*/
|
*/
|
||||||
const ContactPointReceiverSummary = ({ receivers }: ContactPointReceiverSummaryProps) => {
|
export const ContactPointReceiverSummary = ({ receivers }: ContactPointReceiverSummaryProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const countByType = groupBy(receivers, (receiver) => receiver.type);
|
const countByType = groupBy(receivers, (receiver) => receiver.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.integrationWrapper}>
|
|
||||||
<Stack direction="column" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<Stack direction="row" alignItems="center" gap={1}>
|
<Stack direction="row" alignItems="center" gap={1}>
|
||||||
{Object.entries(countByType).map(([type, receivers], index) => {
|
{Object.entries(countByType).map(([type, receivers], index) => {
|
||||||
@ -510,7 +508,7 @@ const ContactPointReceiverSummary = ({ receivers }: ContactPointReceiverSummaryP
|
|||||||
<React.Fragment key={type}>
|
<React.Fragment key={type}>
|
||||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||||
{iconName && <Icon name={iconName} />}
|
{iconName && <Icon name={iconName} />}
|
||||||
<Text variant="body" color="primary">
|
<Text variant="body">
|
||||||
{receiverName}
|
{receiverName}
|
||||||
{receivers.length > 1 && <> ({receivers.length})</>}
|
{receivers.length > 1 && <> ({receivers.length})</>}
|
||||||
</Text>
|
</Text>
|
||||||
@ -521,7 +519,6 @@ const ContactPointReceiverSummary = ({ receivers }: ContactPointReceiverSummaryP
|
|||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export const AlertRuleNameInput = () => {
|
|||||||
register,
|
register,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const ruleFormType = watch('type');
|
const ruleFormType = watch('type');
|
||||||
const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
|
const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
|
||||||
|
@ -3,19 +3,7 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form';
|
import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import {
|
import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||||
Button,
|
|
||||||
Field,
|
|
||||||
InlineLabel,
|
|
||||||
Label,
|
|
||||||
useStyles2,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
LoadingPlaceholder,
|
|
||||||
Stack,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
|
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
@ -23,6 +11,8 @@ import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions';
|
|||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { RuleFormValues } from '../../types/rule-form';
|
||||||
import AlertLabelDropdown from '../AlertLabelDropdown';
|
import AlertLabelDropdown from '../AlertLabelDropdown';
|
||||||
|
|
||||||
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
dataSourceName?: string | null;
|
dataSourceName?: string | null;
|
||||||
@ -271,23 +261,20 @@ const LabelsField: FC<Props> = ({ dataSourceName }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Label description="A set of default labels is automatically added. Add additional labels as required.">
|
<Stack direction="column" gap={1}>
|
||||||
<Stack gap={0.5} alignItems="center">
|
<Text element="h5">Labels</Text>
|
||||||
<Text variant="bodySmall" color="primary">
|
<Stack direction={'row'} gap={1}>
|
||||||
Labels
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
Add labels to your rule to annotate your rules, ease searching, or route to a notification policy.
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
<NeedHelpInfo
|
||||||
content={
|
contentText="The dropdown only displays labels that you have previously used for alerts.
|
||||||
<div>
|
Select a label from the options below or type in a new one."
|
||||||
The dropdown only displays labels that you have previously used for alerts. Select a label from the
|
title="Labels"
|
||||||
dropdown or type in a new one.
|
/>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon className={styles.icon} name="info-circle" size="sm" />
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Label>
|
</Stack>
|
||||||
|
<div className={styles.labelsContainer}></div>
|
||||||
{dataSourceName ? <LabelsWithSuggestions dataSourceName={dataSourceName} /> : <LabelsWithoutSuggestions />}
|
{dataSourceName ? <LabelsWithSuggestions dataSourceName={dataSourceName} /> : <LabelsWithoutSuggestions />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -295,47 +282,48 @@ const LabelsField: FC<Props> = ({ dataSourceName }) => {
|
|||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
icon: css`
|
icon: css({
|
||||||
margin-right: ${theme.spacing(0.5)};
|
marginRight: theme.spacing(0.5),
|
||||||
`,
|
}),
|
||||||
flexColumn: css`
|
flexColumn: css({
|
||||||
display: flex;
|
display: 'flex',
|
||||||
flex-direction: column;
|
flexDirection: 'column',
|
||||||
`,
|
}),
|
||||||
flexRow: css`
|
flexRow: css({
|
||||||
display: flex;
|
display: 'flex',
|
||||||
flex-direction: row;
|
flexDirection: 'row',
|
||||||
justify-content: flex-start;
|
justifyContent: 'flex-start',
|
||||||
|
'& + button': {
|
||||||
& + button {
|
marginLeft: theme.spacing(0.5),
|
||||||
margin-left: ${theme.spacing(0.5)};
|
},
|
||||||
}
|
}),
|
||||||
`,
|
deleteLabelButton: css({
|
||||||
deleteLabelButton: css`
|
marginLeft: theme.spacing(0.5),
|
||||||
margin-left: ${theme.spacing(0.5)};
|
alignSelf: 'flex-start',
|
||||||
align-self: flex-start;
|
}),
|
||||||
`,
|
addLabelButton: css({
|
||||||
addLabelButton: css`
|
flexGrow: 0,
|
||||||
flex-grow: 0;
|
alignSelf: 'flex-start',
|
||||||
align-self: flex-start;
|
}),
|
||||||
`,
|
centerAlignRow: css({
|
||||||
centerAlignRow: css`
|
alignItems: 'baseline',
|
||||||
align-items: baseline;
|
}),
|
||||||
`,
|
equalSign: css({
|
||||||
equalSign: css`
|
alignSelf: 'flex-start',
|
||||||
align-self: flex-start;
|
width: '28px',
|
||||||
width: 28px;
|
justifyContent: 'center',
|
||||||
justify-content: center;
|
marginLeft: theme.spacing(0.5),
|
||||||
margin-left: ${theme.spacing(0.5)};
|
}),
|
||||||
`,
|
labelInput: css({
|
||||||
labelInput: css`
|
width: '175px',
|
||||||
width: 175px;
|
marginBottom: `-${theme.spacing(1)}`,
|
||||||
margin-bottom: -${theme.spacing(1)};
|
'& + &': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
& + & {
|
},
|
||||||
margin-left: ${theme.spacing(1)};
|
}),
|
||||||
}
|
labelsContainer: css({
|
||||||
`,
|
marginBottom: theme.spacing(3),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { Icon, Text, Stack } from '@grafana/ui';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Icon, RadioButtonGroup, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
@ -9,42 +12,152 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
|||||||
import LabelsField from './LabelsField';
|
import LabelsField from './LabelsField';
|
||||||
import { NeedHelpInfo } from './NeedHelpInfo';
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
|
import { SimplifiedRouting } from './alert-rule-form/simplifiedRouting/SimplifiedRouting';
|
||||||
import { NotificationPreview } from './notificaton-preview/NotificationPreview';
|
import { NotificationPreview } from './notificaton-preview/NotificationPreview';
|
||||||
|
|
||||||
type NotificationsStepProps = {
|
type NotificationsStepProps = {
|
||||||
alertUid?: string;
|
alertUid?: string;
|
||||||
};
|
};
|
||||||
export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
|
||||||
const { watch } = useFormContext<RuleFormValues & { location?: string }>();
|
|
||||||
|
|
||||||
const [type, labels, queries, condition, folder, alertName] = watch([
|
enum RoutingOptions {
|
||||||
|
NotificationPolicy = 'notification policy',
|
||||||
|
ContactPoint = 'contact point',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||||
|
const { watch, setValue } = useFormContext<RuleFormValues>();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const [type, labels, queries, condition, folder, alertName, manualRouting] = watch([
|
||||||
'type',
|
'type',
|
||||||
'labels',
|
'labels',
|
||||||
'queries',
|
'queries',
|
||||||
'condition',
|
'condition',
|
||||||
'folder',
|
'folder',
|
||||||
'name',
|
'name',
|
||||||
|
'manualRouting',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME;
|
const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME;
|
||||||
|
|
||||||
const shouldRenderPreview = type === RuleFormType.grafana;
|
const shouldRenderPreview = type === RuleFormType.grafana;
|
||||||
|
|
||||||
const NotificationsStepDescription = () => {
|
const routingOptions = [
|
||||||
|
{ label: 'Manually select contact point', value: RoutingOptions.ContactPoint },
|
||||||
|
{ label: 'Auto-select contact point', value: RoutingOptions.NotificationPolicy },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onRoutingOptionChange = (option: RoutingOptions) => {
|
||||||
|
setValue('manualRouting', option === RoutingOptions.ContactPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
|
||||||
|
|
||||||
|
const shouldAllowSimplifiedRouting = type === RuleFormType.grafana && simplifiedRoutingToggleEnabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RuleEditorSection
|
||||||
|
stepNo={type === RuleFormType.cloudRecording ? 4 : 5}
|
||||||
|
title={type === RuleFormType.cloudRecording ? 'Add labels' : 'Labels and notifications'}
|
||||||
|
description={
|
||||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||||
|
{type === RuleFormType.cloudRecording ? (
|
||||||
<Text variant="bodySmall" color="secondary">
|
<Text variant="bodySmall" color="secondary">
|
||||||
Add custom labels to change the way your notifications are routed.
|
Add labels to help you better manage your recording rules
|
||||||
</Text>
|
</Text>
|
||||||
|
) : (
|
||||||
|
shouldAllowSimplifiedRouting && (
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
Select who should receive a notification when an alert rule fires.
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<LabelsField dataSourceName={dataSourceName} />
|
||||||
|
{shouldAllowSimplifiedRouting && (
|
||||||
|
<div className={styles.configureNotifications}>
|
||||||
|
<Text element="h5">Configure notifications</Text>
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
Select who should receive a notification when an alert rule fires.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<RuleEditorSectionBody />
|
||||||
|
</RuleEditorSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is used to render the section body of the NotificationsStep, depending on the routing option selected.
|
||||||
|
* If simplified routing is not enabled, it will render the NotificationPreview component.
|
||||||
|
* If simplified routing is enabled, it will render the switch between the manual routing and the notification policy routing.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function RuleEditorSectionBody() {
|
||||||
|
if (!shouldAllowSimplifiedRouting) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{shouldRenderPreview && (
|
||||||
|
<NotificationPreview
|
||||||
|
alertQueries={queries}
|
||||||
|
customLabels={labels}
|
||||||
|
condition={condition}
|
||||||
|
folder={folder}
|
||||||
|
alertName={alertName}
|
||||||
|
alertUid={alertUid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack direction="column">
|
||||||
|
<Stack direction="column">
|
||||||
|
<RadioButtonGroup
|
||||||
|
options={routingOptions}
|
||||||
|
value={manualRouting ? RoutingOptions.ContactPoint : RoutingOptions.NotificationPolicy}
|
||||||
|
onChange={onRoutingOptionChange}
|
||||||
|
className={styles.routingOptions}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<RoutingOptionDescription manualRouting={manualRouting} />
|
||||||
|
|
||||||
|
{manualRouting ? (
|
||||||
|
<div className={styles.simplifiedRouting}>
|
||||||
|
<SimplifiedRouting />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
shouldRenderPreview && (
|
||||||
|
<NotificationPreview
|
||||||
|
alertQueries={queries}
|
||||||
|
customLabels={labels}
|
||||||
|
condition={condition}
|
||||||
|
folder={folder}
|
||||||
|
alertName={alertName}
|
||||||
|
alertUid={alertUid}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auxiliar components to build the texts and descriptions in the NotificationsStep
|
||||||
|
function NeedHelpInfoForNotificationPolicy() {
|
||||||
|
return (
|
||||||
<NeedHelpInfo
|
<NeedHelpInfo
|
||||||
contentText={
|
contentText={
|
||||||
<Stack gap={1}>
|
<Stack gap={1} direction="column">
|
||||||
<Stack direction="row" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<>
|
<>
|
||||||
Firing alert rule instances are routed to notification policies based on matching labels. All alert
|
Firing alert rule instances are routed to notification policies based on matching labels. All alert rules
|
||||||
rules and instances, irrespective of their labels, match the default notification policy. If there are
|
and instances, irrespective of their labels, match the default notification policy. If there are no nested
|
||||||
no nested policies, or no nested policies match the labels in the alert rule or alert instance, then
|
policies, or no nested policies match the labels in the alert rule or alert instance, then the default
|
||||||
the default notification policy is the matching policy.
|
notification policy is the matching policy.
|
||||||
</>
|
</>
|
||||||
<a
|
<a
|
||||||
href={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/notification-policies/notifications/`}
|
href={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/notification-policies/notifications/`}
|
||||||
@ -56,10 +169,10 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<>
|
<>
|
||||||
Custom labels change the way your notifications are routed. First, add labels to your alert rule and
|
Custom labels change the way your notifications are routed. First, add labels to your alert rule and then
|
||||||
then connect them to your notification policy by adding label matchers.
|
connect them to your notification policy by adding label matchers.
|
||||||
</>
|
</>
|
||||||
<a
|
<a
|
||||||
href={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/annotation-label/`}
|
href={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/annotation-label/`}
|
||||||
@ -75,38 +188,76 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
|||||||
}
|
}
|
||||||
title="Notification routing"
|
title="Notification routing"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function NeedHelpInfoForContactpoint() {
|
||||||
return (
|
return (
|
||||||
<RuleEditorSection
|
<NeedHelpInfo
|
||||||
stepNo={type === RuleFormType.cloudRecording ? 4 : 5}
|
contentText={
|
||||||
title={type === RuleFormType.cloudRecording ? 'Add labels' : 'Configure notifications'}
|
<>
|
||||||
description={
|
Select a contact point to notify all recipients in it.
|
||||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
<br />
|
||||||
{type === RuleFormType.cloudRecording ? (
|
<br />
|
||||||
<Text variant="bodySmall" color="secondary">
|
Notifications for firing alert instances are grouped based on folder and alert rule name.
|
||||||
Add labels to help you better manage your recording rules
|
<br />
|
||||||
</Text>
|
The waiting time until the initial notification is sent for a new group created by an incoming alert is 30
|
||||||
) : (
|
seconds.
|
||||||
<NotificationsStepDescription />
|
<br />
|
||||||
)}
|
The waiting time to send a batch of new alerts for that group after the first notification was sent is 5
|
||||||
</Stack>
|
minutes.
|
||||||
|
<br />
|
||||||
|
The waiting time to resend an alert after they have successfully been sent is 4 hours.
|
||||||
|
<br />
|
||||||
|
Grouping and wait time values are defined in your default notification policy.
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
fullWidth
|
// todo: update the link with the new documentation about simplified routing
|
||||||
>
|
externalLink="`https://grafana.com/docs/grafana/latest/alerting/fundamentals/notification-policies/notifications/`"
|
||||||
<LabelsField dataSourceName={dataSourceName} />
|
linkText="Read more about notifiying contact points"
|
||||||
{shouldRenderPreview && (
|
title="Notify contact points"
|
||||||
<NotificationPreview
|
|
||||||
alertQueries={queries}
|
|
||||||
customLabels={labels}
|
|
||||||
condition={condition}
|
|
||||||
folder={folder}
|
|
||||||
alertName={alertName}
|
|
||||||
alertUid={alertUid}
|
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
</RuleEditorSection>
|
}
|
||||||
|
interface NotificationsStepDescriptionProps {
|
||||||
|
manualRouting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoutingOptionDescription = ({ manualRouting }: NotificationsStepDescriptionProps) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
return (
|
||||||
|
<div className={styles.notificationsOptionDescription}>
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
{manualRouting
|
||||||
|
? 'Notifications for firing alerts are routed to a selected contact point.'
|
||||||
|
: 'Notifications for firing alerts are routed to contact points based on matching labels.'}
|
||||||
|
</Text>
|
||||||
|
{manualRouting ? <NeedHelpInfoForContactpoint /> : <NeedHelpInfoForNotificationPolicy />}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
routingOptions: css({
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
width: 'fit-content',
|
||||||
|
}),
|
||||||
|
simplifiedRouting: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
configureNotifications: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
notificationsOptionDescription: css({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
@ -7,7 +7,7 @@ import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
|||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
|
|
||||||
export function RecordingRulesNameSpaceAndGroupStep() {
|
export function RecordingRulesNameSpaceAndGroupStep() {
|
||||||
const { watch } = useFormContext<RuleFormValues & { location?: string }>();
|
const { watch } = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const dataSourceName = watch('dataSourceName');
|
const dataSourceName = watch('dataSourceName');
|
||||||
|
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
|
import { Alert, Field, LoadingPlaceholder, Select, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource';
|
||||||
|
|
||||||
|
import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints.v2';
|
||||||
|
import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints';
|
||||||
|
|
||||||
|
import { selectContactPoint } from './SimplifiedRouting';
|
||||||
|
|
||||||
|
export interface ContactPointSelectorProps {
|
||||||
|
alertManager: AlertManagerDataSource;
|
||||||
|
selectedReceiver?: string;
|
||||||
|
dispatch: React.Dispatch<AnyAction>;
|
||||||
|
}
|
||||||
|
export function ContactPointSelector({ selectedReceiver, alertManager, dispatch }: ContactPointSelectorProps) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const onChange = (value: SelectableValue<string>) => {
|
||||||
|
dispatch(selectContactPoint({ receiver: value?.value, alertManager }));
|
||||||
|
};
|
||||||
|
const { isLoading, error, contactPoints: receivers } = useContactPointsWithStatus();
|
||||||
|
const options = receivers.map((receiver) => {
|
||||||
|
const integrations = receiver?.grafana_managed_receiver_configs;
|
||||||
|
const description = <ContactPointReceiverSummary receivers={integrations ?? []} />;
|
||||||
|
|
||||||
|
return { label: receiver.name, value: receiver.name, description };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Alert title="Failed to fetch contact points" severity="error" />;
|
||||||
|
}
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingPlaceholder text={'Loading...'} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column">
|
||||||
|
<Field label="Contact point">
|
||||||
|
<div className={styles.contactPointsSelector}>
|
||||||
|
<Select
|
||||||
|
aria-label="Contact point"
|
||||||
|
onChange={onChange}
|
||||||
|
// We are passing a JSX.Element into the "description" for options, which isn't how the TS typings are defined.
|
||||||
|
// The regular Select component will render it just fine, but we can't update the typings because SelectableValue
|
||||||
|
// is shared with other components where the "description" _has_ to be a string.
|
||||||
|
// I've tried unsuccessfully to separate the typings just I'm giving up :'(
|
||||||
|
// @ts-ignore
|
||||||
|
options={options}
|
||||||
|
width={50}
|
||||||
|
value={selectedReceiver}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
contactPointsSelector: css({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
});
|
@ -0,0 +1,153 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import React, { useEffect, useReducer } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||||
|
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||||
|
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
|
||||||
|
import {
|
||||||
|
AlertManagerDataSource,
|
||||||
|
getAlertManagerDataSourcesByPermission,
|
||||||
|
} from 'app/features/alerting/unified/utils/datasource';
|
||||||
|
import { createUrl } from 'app/features/alerting/unified/utils/url';
|
||||||
|
|
||||||
|
import { ContactPointSelector } from './ContactPointSelector';
|
||||||
|
|
||||||
|
export interface AMContactPoint {
|
||||||
|
alertManager: AlertManagerDataSource;
|
||||||
|
selectedContactPoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectContactPoint = createAction<{ receiver: string | undefined; alertManager: AlertManagerDataSource }>(
|
||||||
|
'simplifiedRouting/selectContactPoint'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const receiversReducer = createReducer<AMContactPoint[]>([], (builder) => {
|
||||||
|
builder.addCase(selectContactPoint, (state, action) => {
|
||||||
|
const { receiver, alertManager } = action.payload;
|
||||||
|
const newContactPoint: AMContactPoint = { selectedContactPoint: receiver, alertManager };
|
||||||
|
const existingContactPoint = state.find((cp) => cp.alertManager.name === alertManager.name);
|
||||||
|
|
||||||
|
if (existingContactPoint) {
|
||||||
|
existingContactPoint.selectedContactPoint = receiver;
|
||||||
|
} else {
|
||||||
|
state.push(newContactPoint);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function SimplifiedRouting() {
|
||||||
|
const { getValues, setValue } = useFormContext<RuleFormValues>();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const contactPointsInAlert = getValues('contactPoints');
|
||||||
|
|
||||||
|
const allAlertManagersByPermission = getAlertManagerDataSourcesByPermission('notification');
|
||||||
|
|
||||||
|
// We decided to only show internal alert manager for now. Once we want to show external alert managers we can use this code
|
||||||
|
// const alertManagersDataSources = allAlertManagersByPermission.availableInternalDataSources.concat(
|
||||||
|
// allAlertManagersByPermission.availableExternalDataSources
|
||||||
|
// );
|
||||||
|
|
||||||
|
const alertManagersDataSources = allAlertManagersByPermission.availableInternalDataSources;
|
||||||
|
|
||||||
|
const alertManagersDataSourcesWithConfigAPI = alertManagersDataSources.filter((am) => am.hasConfigurationAPI);
|
||||||
|
|
||||||
|
// we merge the selected contact points with the alert manager meta data
|
||||||
|
const alertManagersWithSelectedContactPoints = alertManagersDataSourcesWithConfigAPI.map((am) => {
|
||||||
|
const selectedContactPoint = contactPointsInAlert?.find((cp) => cp.alertManager === am.name);
|
||||||
|
return { alertManager: am, selectedContactPoint: selectedContactPoint?.selectedContactPoint };
|
||||||
|
});
|
||||||
|
|
||||||
|
// use reducer to keep this alertManagersWithSelectedContactPoints in the state
|
||||||
|
const [alertManagersWithCPState, dispatch] = useReducer(receiversReducer, alertManagersWithSelectedContactPoints);
|
||||||
|
|
||||||
|
function getContactPointsForForm(alertManagersWithCP: AMContactPoint[]) {
|
||||||
|
return alertManagersWithCP.map((am) => {
|
||||||
|
return { alertManager: am.alertManager.name, selectedContactPoint: am.selectedContactPoint };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// whenever we update the receiversState we have to update the form too
|
||||||
|
useEffect(() => {
|
||||||
|
const contactPointsForForm = getContactPointsForForm(alertManagersWithCPState);
|
||||||
|
setValue('contactPoints', contactPointsForForm, { shouldValidate: false });
|
||||||
|
}, [alertManagersWithCPState, setValue]);
|
||||||
|
|
||||||
|
const shouldShowAM = true;
|
||||||
|
|
||||||
|
return alertManagersWithCPState.map((alertManagerContactPoint, index) => {
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
<Stack direction="column">
|
||||||
|
{shouldShowAM && (
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<div className={styles.firstAlertManagerLine}></div>
|
||||||
|
<div className={styles.alertManagerName}>
|
||||||
|
{' '}
|
||||||
|
Alert manager:
|
||||||
|
<img
|
||||||
|
src={alertManagerContactPoint.alertManager.imgUrl}
|
||||||
|
alt="Alert manager logo"
|
||||||
|
className={styles.img}
|
||||||
|
/>
|
||||||
|
{alertManagerContactPoint.alertManager.name}
|
||||||
|
</div>
|
||||||
|
<div className={styles.secondAlertManagerLine}></div>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Stack direction="row" gap={1} alignItems="center">
|
||||||
|
<AlertmanagerProvider
|
||||||
|
accessType={'notification'}
|
||||||
|
alertmanagerSourceName={alertManagerContactPoint.alertManager.name}
|
||||||
|
>
|
||||||
|
<ContactPointSelector
|
||||||
|
selectedReceiver={alertManagerContactPoint.selectedContactPoint}
|
||||||
|
dispatch={dispatch}
|
||||||
|
alertManager={alertManagerContactPoint.alertManager}
|
||||||
|
/>
|
||||||
|
</AlertmanagerProvider>
|
||||||
|
<LinkToContactPoints />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkToContactPoints() {
|
||||||
|
const hrefToContactPoints = '/alerting/notifications';
|
||||||
|
return (
|
||||||
|
<Link target="_blank" href={createUrl(hrefToContactPoints)} rel="noopener" aria-label="View alert rule">
|
||||||
|
<Stack direction="row" gap={1} alignItems="center" justifyContent="center">
|
||||||
|
<Text color="secondary">To browse contact points and create new ones go to</Text>
|
||||||
|
<Text color="link">Contact points</Text>
|
||||||
|
<Icon name={'external-link-alt'} size="sm" color="link" />
|
||||||
|
</Stack>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
firstAlertManagerLine: css({
|
||||||
|
height: 1,
|
||||||
|
width: theme.spacing(4),
|
||||||
|
backgroundColor: theme.colors.secondary.main,
|
||||||
|
}),
|
||||||
|
alertManagerName: css({
|
||||||
|
with: 'fit-content',
|
||||||
|
}),
|
||||||
|
secondAlertManagerLine: css({
|
||||||
|
height: '1px',
|
||||||
|
width: '100%',
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.colors.secondary.main,
|
||||||
|
}),
|
||||||
|
img: css({
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
width: theme.spacing(3),
|
||||||
|
height: theme.spacing(3),
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
});
|
@ -13,30 +13,28 @@ import { mockApi, setupMswServer } from '../../../mockApi';
|
|||||||
import { grantUserPermissions, mockAlertQuery } from '../../../mocks';
|
import { grantUserPermissions, mockAlertQuery } from '../../../mocks';
|
||||||
import { mockPreviewApiResponse } from '../../../mocks/alertRuleApi';
|
import { mockPreviewApiResponse } from '../../../mocks/alertRuleApi';
|
||||||
import * as dataSource from '../../../utils/datasource';
|
import * as dataSource from '../../../utils/datasource';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
import {
|
||||||
|
AlertManagerDataSource,
|
||||||
|
GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
useGetAlertManagerDataSourcesByPermissionAndConfig,
|
||||||
|
} from '../../../utils/datasource';
|
||||||
import { Folder } from '../RuleFolderPicker';
|
import { Folder } from '../RuleFolderPicker';
|
||||||
|
|
||||||
import { NotificationPreview } from './NotificationPreview';
|
import { NotificationPreview } from './NotificationPreview';
|
||||||
import NotificationPreviewByAlertManager from './NotificationPreviewByAlertManager';
|
import NotificationPreviewByAlertManager from './NotificationPreviewByAlertManager';
|
||||||
import * as notificationPreview from './useGetAlertManagersSourceNamesAndImage';
|
|
||||||
import { useGetAlertManagersSourceNamesAndImage } from './useGetAlertManagersSourceNamesAndImage';
|
|
||||||
|
|
||||||
jest.mock('../../../useRouteGroupsMatcher');
|
jest.mock('../../../useRouteGroupsMatcher');
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(notificationPreview, 'useGetAlertManagersSourceNamesAndImage')
|
.spyOn(dataSource, 'useGetAlertManagerDataSourcesByPermissionAndConfig')
|
||||||
.mockReturnValue([{ name: GRAFANA_RULES_SOURCE_NAME, img: '' }]);
|
.mockReturnValue([{ name: GRAFANA_RULES_SOURCE_NAME, imgUrl: '', hasConfigurationAPI: true }]);
|
||||||
|
|
||||||
jest.spyOn(notificationPreview, 'useGetAlertManagersSourceNamesAndImage').mockReturnValue([
|
|
||||||
{ name: GRAFANA_RULES_SOURCE_NAME, img: '' },
|
|
||||||
{ name: GRAFANA_RULES_SOURCE_NAME, img: '' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
jest.spyOn(dataSource, 'getDatasourceAPIUid').mockImplementation((ds: string) => ds);
|
jest.spyOn(dataSource, 'getDatasourceAPIUid').mockImplementation((ds: string) => ds);
|
||||||
|
|
||||||
const useGetAlertManagersSourceNamesAndImageMock = useGetAlertManagersSourceNamesAndImage as jest.MockedFunction<
|
const getAlertManagerDataSourcesByPermissionAndConfigMock =
|
||||||
typeof useGetAlertManagersSourceNamesAndImage
|
useGetAlertManagerDataSourcesByPermissionAndConfig as jest.MockedFunction<
|
||||||
>;
|
typeof useGetAlertManagerDataSourcesByPermissionAndConfig
|
||||||
|
>;
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
route: byTestId('matching-policy-route'),
|
route: byTestId('matching-policy-route'),
|
||||||
@ -62,8 +60,14 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const alertQuery = mockAlertQuery({ datasourceUid: 'whatever', refId: 'A' });
|
const alertQuery = mockAlertQuery({ datasourceUid: 'whatever', refId: 'A' });
|
||||||
|
|
||||||
|
const grafanaAlertManagerDataSource: AlertManagerDataSource = {
|
||||||
|
name: GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
imgUrl: '',
|
||||||
|
hasConfigurationAPI: true,
|
||||||
|
};
|
||||||
|
|
||||||
function mockOneAlertManager() {
|
function mockOneAlertManager() {
|
||||||
useGetAlertManagersSourceNamesAndImageMock.mockReturnValue([{ name: GRAFANA_RULES_SOURCE_NAME, img: '' }]);
|
getAlertManagerDataSourcesByPermissionAndConfigMock.mockReturnValue([grafanaAlertManagerDataSource]);
|
||||||
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
|
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
|
||||||
amConfigBuilder
|
amConfigBuilder
|
||||||
.withRoute((routeBuilder) =>
|
.withRoute((routeBuilder) =>
|
||||||
@ -79,10 +83,11 @@ function mockOneAlertManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mockTwoAlertManagers() {
|
function mockTwoAlertManagers() {
|
||||||
useGetAlertManagersSourceNamesAndImageMock.mockReturnValue([
|
getAlertManagerDataSourcesByPermissionAndConfigMock.mockReturnValue([
|
||||||
{ name: GRAFANA_RULES_SOURCE_NAME, img: '' },
|
{ name: 'OTHER_AM', imgUrl: '', hasConfigurationAPI: true },
|
||||||
{ name: 'OTHER_AM', img: '' },
|
grafanaAlertManagerDataSource,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
|
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
|
||||||
amConfigBuilder
|
amConfigBuilder
|
||||||
.withRoute((routeBuilder) =>
|
.withRoute((routeBuilder) =>
|
||||||
@ -272,7 +277,7 @@ describe('NotificationPreviewByAlertmanager', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<NotificationPreviewByAlertManager
|
<NotificationPreviewByAlertManager
|
||||||
alertManagerSource={{ name: GRAFANA_RULES_SOURCE_NAME, img: '' }}
|
alertManagerSource={grafanaAlertManagerDataSource}
|
||||||
potentialInstances={potentialInstances}
|
potentialInstances={potentialInstances}
|
||||||
onlyOneAM={true}
|
onlyOneAM={true}
|
||||||
/>,
|
/>,
|
||||||
@ -327,7 +332,7 @@ describe('NotificationPreviewByAlertmanager', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<NotificationPreviewByAlertManager
|
<NotificationPreviewByAlertManager
|
||||||
alertManagerSource={{ name: GRAFANA_RULES_SOURCE_NAME, img: '' }}
|
alertManagerSource={grafanaAlertManagerDataSource}
|
||||||
potentialInstances={potentialInstances}
|
potentialInstances={potentialInstances}
|
||||||
onlyOneAM={true}
|
onlyOneAM={true}
|
||||||
/>,
|
/>,
|
||||||
@ -382,7 +387,7 @@ describe('NotificationPreviewByAlertmanager', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<NotificationPreviewByAlertManager
|
<NotificationPreviewByAlertManager
|
||||||
alertManagerSource={{ name: GRAFANA_RULES_SOURCE_NAME, img: '' }}
|
alertManagerSource={grafanaAlertManagerDataSource}
|
||||||
potentialInstances={potentialInstances}
|
potentialInstances={potentialInstances}
|
||||||
onlyOneAM={true}
|
onlyOneAM={true}
|
||||||
/>,
|
/>,
|
||||||
|
@ -3,15 +3,14 @@ import { compact } from 'lodash';
|
|||||||
import React, { lazy, Suspense } from 'react';
|
import React, { lazy, Suspense } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, LoadingPlaceholder, useStyles2, Text } from '@grafana/ui';
|
import { Button, LoadingPlaceholder, Text, useStyles2 } from '@grafana/ui';
|
||||||
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
|
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
|
||||||
import { Stack } from 'app/plugins/datasource/parca/QueryEditor/Stack';
|
import { Stack } from 'app/plugins/datasource/parca/QueryEditor/Stack';
|
||||||
import { AlertQuery } from 'app/types/unified-alerting-dto';
|
import { AlertQuery } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { useGetAlertManagerDataSourcesByPermissionAndConfig } from '../../../utils/datasource';
|
||||||
import { Folder } from '../RuleFolderPicker';
|
import { Folder } from '../RuleFolderPicker';
|
||||||
|
|
||||||
import { useGetAlertManagersSourceNamesAndImage } from './useGetAlertManagersSourceNamesAndImage';
|
|
||||||
|
|
||||||
const NotificationPreviewByAlertManager = lazy(() => import('./NotificationPreviewByAlertManager'));
|
const NotificationPreviewByAlertManager = lazy(() => import('./NotificationPreviewByAlertManager'));
|
||||||
|
|
||||||
interface NotificationPreviewProps {
|
interface NotificationPreviewProps {
|
||||||
@ -63,10 +62,10 @@ export const NotificationPreview = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get list of alert managers source name + image
|
// Get alert managers's data source information
|
||||||
const alertManagerSourceNamesAndImage = useGetAlertManagersSourceNamesAndImage();
|
const alertManagerDataSources = useGetAlertManagerDataSourcesByPermissionAndConfig('notification');
|
||||||
|
|
||||||
const onlyOneAM = alertManagerSourceNamesAndImage.length === 1;
|
const onlyOneAM = alertManagerDataSources.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
@ -98,7 +97,7 @@ export const NotificationPreview = ({
|
|||||||
</div>
|
</div>
|
||||||
{!isLoading && !previewUninitialized && potentialInstances.length > 0 && (
|
{!isLoading && !previewUninitialized && potentialInstances.length > 0 && (
|
||||||
<Suspense fallback={<LoadingPlaceholder text="Loading preview..." />}>
|
<Suspense fallback={<LoadingPlaceholder text="Loading preview..." />}>
|
||||||
{alertManagerSourceNamesAndImage.map((alertManagerSource) => (
|
{alertManagerDataSources.map((alertManagerSource) => (
|
||||||
<NotificationPreviewByAlertManager
|
<NotificationPreviewByAlertManager
|
||||||
alertManagerSource={alertManagerSource}
|
alertManagerSource={alertManagerSource}
|
||||||
potentialInstances={potentialInstances}
|
potentialInstances={potentialInstances}
|
||||||
|
@ -6,17 +6,17 @@ import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafa
|
|||||||
|
|
||||||
import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack';
|
import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack';
|
||||||
import { Labels } from '../../../../../../types/unified-alerting-dto';
|
import { Labels } from '../../../../../../types/unified-alerting-dto';
|
||||||
|
import { AlertManagerDataSource } from '../../../utils/datasource';
|
||||||
|
|
||||||
import { NotificationRoute } from './NotificationRoute';
|
import { NotificationRoute } from './NotificationRoute';
|
||||||
import { useAlertmanagerNotificationRoutingPreview } from './useAlertmanagerNotificationRoutingPreview';
|
import { useAlertmanagerNotificationRoutingPreview } from './useAlertmanagerNotificationRoutingPreview';
|
||||||
import { AlertManagerNameWithImage } from './useGetAlertManagersSourceNamesAndImage';
|
|
||||||
|
|
||||||
function NotificationPreviewByAlertManager({
|
function NotificationPreviewByAlertManager({
|
||||||
alertManagerSource,
|
alertManagerSource,
|
||||||
potentialInstances,
|
potentialInstances,
|
||||||
onlyOneAM,
|
onlyOneAM,
|
||||||
}: {
|
}: {
|
||||||
alertManagerSource: AlertManagerNameWithImage;
|
alertManagerSource: AlertManagerDataSource;
|
||||||
potentialInstances: Labels[];
|
potentialInstances: Labels[];
|
||||||
onlyOneAM: boolean;
|
onlyOneAM: boolean;
|
||||||
}) {
|
}) {
|
||||||
@ -49,7 +49,7 @@ function NotificationPreviewByAlertManager({
|
|||||||
<div className={styles.alertManagerName}>
|
<div className={styles.alertManagerName}>
|
||||||
{' '}
|
{' '}
|
||||||
Alert manager:
|
Alert manager:
|
||||||
<img src={alertManagerSource.img} alt="" className={styles.img} />
|
<img src={alertManagerSource.imgUrl} alt="" className={styles.img} />
|
||||||
{alertManagerSource.name}
|
{alertManagerSource.name}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.secondAlertManagerLine}></div>
|
<div className={styles.secondAlertManagerLine}></div>
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { AlertmanagerChoice } from '../../../../../../plugins/datasource/alertmanager/types';
|
|
||||||
import { alertmanagerApi } from '../../../api/alertmanagerApi';
|
|
||||||
import { getExternalDsAlertManagers, GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
|
||||||
|
|
||||||
export interface AlertManagerNameWithImage {
|
|
||||||
name: string;
|
|
||||||
img: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGetAlertManagersSourceNamesAndImage = () => {
|
|
||||||
//get current alerting config
|
|
||||||
const { currentData: amConfigStatus } = alertmanagerApi.useGetAlertmanagerChoiceStatusQuery(undefined);
|
|
||||||
|
|
||||||
const externalDsAlertManagers = getExternalDsAlertManagers().map((ds) => ({
|
|
||||||
name: ds.name,
|
|
||||||
img: ds.meta.info.logos.small,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const alertmanagerChoice = amConfigStatus?.alertmanagersChoice;
|
|
||||||
const alertManagerSourceNamesWithImage: AlertManagerNameWithImage[] =
|
|
||||||
alertmanagerChoice === AlertmanagerChoice.Internal
|
|
||||||
? [{ name: GRAFANA_RULES_SOURCE_NAME, img: 'public/img/grafana_icon.svg' }]
|
|
||||||
: alertmanagerChoice === AlertmanagerChoice.External
|
|
||||||
? externalDsAlertManagers
|
|
||||||
: [{ name: GRAFANA_RULES_SOURCE_NAME, img: 'public/img/grafana_icon.svg' }, ...externalDsAlertManagers];
|
|
||||||
|
|
||||||
return alertManagerSourceNamesWithImage;
|
|
||||||
};
|
|
@ -18,7 +18,7 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const ruleFormType = watch('type');
|
const ruleFormType = watch('type');
|
||||||
@ -40,8 +40,6 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C
|
|||||||
{...field}
|
{...field}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(ds: DataSourceInstanceSettings) => {
|
onChange={(ds: DataSourceInstanceSettings) => {
|
||||||
// reset location if switching data sources, as different rules source will have different groups and namespaces
|
|
||||||
setValue('location', undefined);
|
|
||||||
// reset expression as they don't need to persist after changing datasources
|
// reset expression as they don't need to persist after changing datasources
|
||||||
setValue('expression', '');
|
setValue('expression', '');
|
||||||
onChange(ds?.name ?? null);
|
onChange(ds?.name ?? null);
|
||||||
|
@ -29,7 +29,9 @@ const externalAmMimir: AlertManagerDataSource = {
|
|||||||
|
|
||||||
describe('useAlertmanager', () => {
|
describe('useAlertmanager', () => {
|
||||||
it('Should return undefined alert manager name when there are no available alert managers', () => {
|
it('Should return undefined alert manager name when there are no available alert managers', () => {
|
||||||
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([]);
|
jest
|
||||||
|
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
||||||
|
.mockReturnValueOnce({ availableExternalDataSources: [], availableInternalDataSources: [] });
|
||||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||||
@ -41,7 +43,11 @@ describe('useAlertmanager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should return Grafana AM when it is available and no alert manager query param exists', () => {
|
it('Should return Grafana AM when it is available and no alert manager query param exists', () => {
|
||||||
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([grafanaAm]);
|
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce({
|
||||||
|
availableExternalDataSources: [],
|
||||||
|
availableInternalDataSources: [{ name: GRAFANA_RULES_SOURCE_NAME, imgUrl: '', hasConfigurationAPI: true }],
|
||||||
|
});
|
||||||
|
|
||||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
<AlertmanagerProvider accessType="instance">{children}</AlertmanagerProvider>
|
||||||
@ -53,7 +59,9 @@ describe('useAlertmanager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should return alert manager included in the query param when available', () => {
|
it('Should return alert manager included in the query param when available', () => {
|
||||||
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([externalAmProm]);
|
jest
|
||||||
|
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
||||||
|
.mockReturnValueOnce({ availableExternalDataSources: [externalAmProm], availableInternalDataSources: [] });
|
||||||
|
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.push({ search: `alertmanager=${externalAmProm.name}` });
|
history.push({ search: `alertmanager=${externalAmProm.name}` });
|
||||||
@ -69,7 +77,9 @@ describe('useAlertmanager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should return undefined if alert manager included in the query is not available', () => {
|
it('Should return undefined if alert manager included in the query is not available', () => {
|
||||||
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([]);
|
jest
|
||||||
|
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
||||||
|
.mockReturnValueOnce({ availableExternalDataSources: [], availableInternalDataSources: [] });
|
||||||
|
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.push({ search: `alertmanager=Not available external AM` });
|
history.push({ search: `alertmanager=Not available external AM` });
|
||||||
@ -85,7 +95,9 @@ describe('useAlertmanager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should return alert manager from store if available and query is empty', () => {
|
it('Should return alert manager from store if available and query is empty', () => {
|
||||||
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce([externalAmProm]);
|
jest
|
||||||
|
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
||||||
|
.mockReturnValueOnce({ availableExternalDataSources: [externalAmProm], availableInternalDataSources: [] });
|
||||||
|
|
||||||
const wrapper = ({ children }: React.PropsWithChildren) => (
|
const wrapper = ({ children }: React.PropsWithChildren) => (
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
@ -100,9 +112,10 @@ describe('useAlertmanager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should prioritize the alert manager from query over store', () => {
|
it('Should prioritize the alert manager from query over store', () => {
|
||||||
jest
|
jest.spyOn(useAlertManagerSources, 'useAlertManagersByPermission').mockReturnValueOnce({
|
||||||
.spyOn(useAlertManagerSources, 'useAlertManagersByPermission')
|
availableExternalDataSources: [externalAmProm, externalAmMimir],
|
||||||
.mockReturnValueOnce([externalAmProm, externalAmMimir]);
|
availableInternalDataSources: [],
|
||||||
|
});
|
||||||
|
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
history.push({ search: `alertmanager=${externalAmProm.name}` });
|
history.push({ search: `alertmanager=${externalAmProm.name}` });
|
||||||
|
@ -8,8 +8,8 @@ import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
|
|||||||
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
|
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants';
|
||||||
import {
|
import {
|
||||||
AlertManagerDataSource,
|
AlertManagerDataSource,
|
||||||
getAlertmanagerDataSourceByName,
|
|
||||||
GRAFANA_RULES_SOURCE_NAME,
|
GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
getAlertmanagerDataSourceByName,
|
||||||
} from '../utils/datasource';
|
} from '../utils/datasource';
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
@ -31,7 +31,10 @@ interface Props extends React.PropsWithChildren {
|
|||||||
|
|
||||||
const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }: Props) => {
|
const AlertmanagerProvider = ({ children, accessType, alertmanagerSourceName }: Props) => {
|
||||||
const [queryParams, updateQueryParams] = useQueryParams();
|
const [queryParams, updateQueryParams] = useQueryParams();
|
||||||
const availableAlertManagers = useAlertManagersByPermission(accessType);
|
const allAvailableAlertManagers = useAlertManagersByPermission(accessType);
|
||||||
|
const availableAlertManagers = allAvailableAlertManagers.availableInternalDataSources.concat(
|
||||||
|
allAvailableAlertManagers.availableExternalDataSources
|
||||||
|
);
|
||||||
|
|
||||||
const updateSelectedAlertmanager = React.useCallback(
|
const updateSelectedAlertmanager = React.useCallback(
|
||||||
(selectedAlertManager: string) => {
|
(selectedAlertManager: string) => {
|
||||||
|
@ -8,6 +8,11 @@ export enum RuleFormType {
|
|||||||
cloudRecording = 'cloud-recording',
|
cloudRecording = 'cloud-recording',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContactPoints {
|
||||||
|
alertManager: string;
|
||||||
|
selectedContactPoint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RuleFormValues {
|
export interface RuleFormValues {
|
||||||
// common
|
// common
|
||||||
name: string;
|
name: string;
|
||||||
@ -27,6 +32,8 @@ export interface RuleFormValues {
|
|||||||
evaluateEvery: string;
|
evaluateEvery: string;
|
||||||
evaluateFor: string;
|
evaluateFor: string;
|
||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
|
contactPoints?: ContactPoints[];
|
||||||
|
manualRouting: boolean;
|
||||||
|
|
||||||
// cortex / loki rules
|
// cortex / loki rules
|
||||||
namespace: string;
|
namespace: string;
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
import {
|
||||||
|
AlertManagerDataSourceJsonData,
|
||||||
|
AlertManagerImplementation,
|
||||||
|
AlertmanagerChoice,
|
||||||
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import { RulesSource } from 'app/types/unified-alerting';
|
import { RulesSource } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||||
|
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
|
||||||
|
import { isAlertManagerWithConfigAPI } from '../state/AlertmanagerContext';
|
||||||
|
|
||||||
import { instancesPermissions, notificationsPermissions } from './access-control';
|
import { instancesPermissions, notificationsPermissions } from './access-control';
|
||||||
import { getAllDataSources } from './config';
|
import { getAllDataSources } from './config';
|
||||||
|
|
||||||
@ -21,6 +29,8 @@ export interface AlertManagerDataSource {
|
|||||||
name: string;
|
name: string;
|
||||||
imgUrl: string;
|
imgUrl: string;
|
||||||
meta?: DataSourceInstanceSettings['meta'];
|
meta?: DataSourceInstanceSettings['meta'];
|
||||||
|
hasConfigurationAPI?: boolean;
|
||||||
|
handleGrafanaManagedAlerts?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus];
|
export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus];
|
||||||
@ -54,6 +64,7 @@ export function getExternalDsAlertManagers() {
|
|||||||
const grafanaAlertManagerDataSource: AlertManagerDataSource = {
|
const grafanaAlertManagerDataSource: AlertManagerDataSource = {
|
||||||
name: GRAFANA_RULES_SOURCE_NAME,
|
name: GRAFANA_RULES_SOURCE_NAME,
|
||||||
imgUrl: 'public/img/grafana_icon.svg',
|
imgUrl: 'public/img/grafana_icon.svg',
|
||||||
|
hasConfigurationAPI: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used only as a fallback for Alert Group plugin
|
// Used only as a fallback for Alert Group plugin
|
||||||
@ -69,17 +80,53 @@ export function getAllAlertManagerDataSources(): AlertManagerDataSource[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAlertManagerDataSourcesByPermission(
|
/**
|
||||||
|
* This method gets all alert managers that the user has access, and then filter them first by being able to handle grafana managed alerts,
|
||||||
|
* and then depending on the current alerting configuration returns either only the internal alert managers, only the external alert managers, or both.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function useGetAlertManagerDataSourcesByPermissionAndConfig(
|
||||||
permission: 'instance' | 'notification'
|
permission: 'instance' | 'notification'
|
||||||
): AlertManagerDataSource[] {
|
): AlertManagerDataSource[] {
|
||||||
const availableDataSources: AlertManagerDataSource[] = [];
|
const allAlertManagersByPermission = useAlertManagersByPermission(permission); // this hook memoizes the result of getAlertManagerDataSourcesByPermission
|
||||||
|
|
||||||
|
const externalDsAlertManagers: AlertManagerDataSource[] =
|
||||||
|
allAlertManagersByPermission.availableExternalDataSources.filter((ds) => ds.handleGrafanaManagedAlerts);
|
||||||
|
const internalDSAlertManagers = allAlertManagersByPermission.availableInternalDataSources;
|
||||||
|
|
||||||
|
//get current alerting configuration
|
||||||
|
const { currentData: amConfigStatus } = alertmanagerApi.useGetAlertmanagerChoiceStatusQuery(undefined);
|
||||||
|
|
||||||
|
const alertmanagerChoice = amConfigStatus?.alertmanagersChoice;
|
||||||
|
|
||||||
|
switch (alertmanagerChoice) {
|
||||||
|
case AlertmanagerChoice.Internal:
|
||||||
|
return internalDSAlertManagers;
|
||||||
|
case AlertmanagerChoice.External:
|
||||||
|
return externalDsAlertManagers;
|
||||||
|
default:
|
||||||
|
return [...internalDSAlertManagers, ...externalDsAlertManagers];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method gets all alert managers that the user has access to and then split them into two groups:
|
||||||
|
* 1. Internal alert managers
|
||||||
|
* 2. External alert managers
|
||||||
|
*/
|
||||||
|
export function getAlertManagerDataSourcesByPermission(permission: 'instance' | 'notification'): {
|
||||||
|
availableInternalDataSources: AlertManagerDataSource[];
|
||||||
|
availableExternalDataSources: AlertManagerDataSource[];
|
||||||
|
} {
|
||||||
|
const availableInternalDataSources: AlertManagerDataSource[] = [];
|
||||||
|
const availableExternalDataSources: AlertManagerDataSource[] = [];
|
||||||
const permissions = {
|
const permissions = {
|
||||||
instance: instancesPermissions.read,
|
instance: instancesPermissions.read,
|
||||||
notification: notificationsPermissions.read,
|
notification: notificationsPermissions.read,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (contextSrv.hasPermission(permissions[permission].grafana)) {
|
if (contextSrv.hasPermission(permissions[permission].grafana)) {
|
||||||
availableDataSources.push(grafanaAlertManagerDataSource);
|
availableInternalDataSources.push(grafanaAlertManagerDataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSrv.hasPermission(permissions[permission].external)) {
|
if (contextSrv.hasPermission(permissions[permission].external)) {
|
||||||
@ -88,11 +135,13 @@ export function getAlertManagerDataSourcesByPermission(
|
|||||||
displayName: ds.name,
|
displayName: ds.name,
|
||||||
imgUrl: ds.meta.info.logos.small,
|
imgUrl: ds.meta.info.logos.small,
|
||||||
meta: ds.meta,
|
meta: ds.meta,
|
||||||
|
hasConfigurationAPI: isAlertManagerWithConfigAPI(ds.jsonData),
|
||||||
|
handleGrafanaManagedAlerts: ds.jsonData.handleGrafanaManagedAlerts,
|
||||||
}));
|
}));
|
||||||
availableDataSources.push(...cloudSources);
|
availableExternalDataSources.push(...cloudSources);
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableDataSources;
|
return { availableInternalDataSources, availableExternalDataSources };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLotexDataSourceByName(dataSourceName: string): DataSourceInstanceSettings {
|
export function getLotexDataSourceByName(dataSourceName: string): DataSourceInstanceSettings {
|
||||||
|
@ -67,6 +67,8 @@ export const getDefaultFormValues = (): RuleFormValues => {
|
|||||||
execErrState: GrafanaAlertStateDecision.Error,
|
execErrState: GrafanaAlertStateDecision.Error,
|
||||||
evaluateFor: '5m',
|
evaluateFor: '5m',
|
||||||
evaluateEvery: MINUTE,
|
evaluateEvery: MINUTE,
|
||||||
|
manualRouting: false, // let's decide this later
|
||||||
|
contactPoints: [],
|
||||||
|
|
||||||
// cortex / loki
|
// cortex / loki
|
||||||
namespace: '',
|
namespace: '',
|
||||||
@ -176,6 +178,8 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
labels: listifyLabelsOrAnnotations(rule.labels, true),
|
||||||
folder: { title: namespace, uid: ga.namespace_uid },
|
folder: { title: namespace, uid: ga.namespace_uid },
|
||||||
isPaused: ga.is_paused,
|
isPaused: ga.is_paused,
|
||||||
|
// manualrouting: ?? //todo depending on the implementation of the manual routing
|
||||||
|
// contactPoints: ?? //todo depending on the implementation of the manual routing
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unexpected type of rule for grafana rules source');
|
throw new Error('Unexpected type of rule for grafana rules source');
|
||||||
|
Loading…
Reference in New Issue
Block a user