Alerting: Make alert rule policies preview use k8s API (#97070)

* Add translations for notification preview

* Make notifications endpoints use alertmanager config mock entity

* Fix translations and error handling in preview component

* Update preview hook to use new k8s APIs

* Move receivers k8s mock logic so it always comes from the mock config

* Fix test that wasn't using the correct receiver

* Fix object_matchers

* Remove mockApi method and update tests

* Update translation for error case

* Remove useMemo
pull/97195/head
Tom Ratcliffe 7 months ago committed by GitHub
parent 1c60d51905
commit a2c407854f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      .betterer.results
  2. 2
      public/app/features/alerting/unified/NotificationPolicies.test.tsx
  3. 147
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx
  4. 21
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx
  5. 12
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreviewByAlertManager.tsx
  6. 70
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts
  7. 22
      public/app/features/alerting/unified/mockApi.ts
  8. 5
      public/app/features/alerting/unified/mocks/server/entities/k8s/routingtrees.ts
  9. 61
      public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts
  10. 18
      public/app/features/alerting/unified/mocks/server/handlers/notifications.ts
  11. 9
      public/locales/en-US/grafana.json
  12. 9
      public/locales/pseudo-LOCALE/grafana.json

@ -1443,16 +1443,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
], ],
"public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
],
"public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreviewByAlertManager.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx:5381": [ "public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],

@ -240,7 +240,7 @@ describe.each([
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, { setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, {
alertmanager_config: { alertmanager_config: {
route: {}, route: {},
receivers: [{ name: 'grafana-default-email' }], receivers: [{ name: 'lotsa-emails' }],
}, },
template_files: {}, template_files: {},
}); });

@ -1,11 +1,13 @@
import { render, screen, userEvent, waitFor, within } from 'test/test-utils'; import { render, screen, waitFor, within } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector'; import { byRole, byTestId, byText } from 'testing-library-selector';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { AccessControlAction } from 'app/types/accessControl'; import { AccessControlAction } from 'app/types/accessControl';
import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types'; import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types';
import { Labels } from '../../../../../../types/unified-alerting-dto'; import { Labels } from '../../../../../../types/unified-alerting-dto';
import { mockApi, setupMswServer } from '../../../mockApi'; import { getMockConfig, setupMswServer } from '../../../mockApi';
import { grantUserPermissions, mockAlertQuery } from '../../../mocks'; import { grantUserPermissions, mockAlertQuery } from '../../../mocks';
import { mockPreviewApiResponse } from '../../../mocks/grafanaRulerApi'; import { mockPreviewApiResponse } from '../../../mocks/grafanaRulerApi';
import { Folder } from '../../../types/rule-form'; import { Folder } from '../../../types/rule-form';
@ -36,7 +38,7 @@ const ui = {
route: byTestId('matching-policy-route'), route: byTestId('matching-policy-route'),
routeButton: byRole('button', { name: /Expand policy route/ }), routeButton: byRole('button', { name: /Expand policy route/ }),
routeMatchingInstances: byTestId('route-matching-instance'), routeMatchingInstances: byTestId('route-matching-instance'),
loadingIndicator: byText(/Loading/), loadingIndicator: byText(/Loading routing preview/i),
previewButton: byRole('button', { name: /preview routing/i }), previewButton: byRole('button', { name: /preview routing/i }),
grafanaAlertManagerLabel: byText(/alertmanager:grafana/i), grafanaAlertManagerLabel: byText(/alertmanager:grafana/i),
otherAlertManagerLabel: byText(/alertmanager:other_am/i), otherAlertManagerLabel: byText(/alertmanager:other_am/i),
@ -62,52 +64,33 @@ const grafanaAlertManagerDataSource: AlertManagerDataSource = {
hasConfigurationAPI: true, hasConfigurationAPI: true,
}; };
const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('slack').addMatcher('tomato', MatcherOperator.equal, 'red'))
.addRoute((rb) => rb.withReceiver('opsgenie').addMatcher('team', MatcherOperator.equal, 'operations'))
)
.addReceivers((b) => b.withName('email').addEmailConfig((eb) => eb.withTo('test@example.com')))
.addReceivers((b) => b.withName('slack'))
.addReceivers((b) => b.withName('opsgenie'))
);
function mockOneAlertManager() { function mockOneAlertManager() {
getAlertManagerDataSourcesByPermissionAndConfigMock.mockReturnValue([grafanaAlertManagerDataSource]); getAlertManagerDataSourcesByPermissionAndConfigMock.mockReturnValue([grafanaAlertManagerDataSource]);
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) =>
amConfigBuilder setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('slack').addMatcher('tomato', MatcherOperator.equal, 'red'))
.addRoute((rb) => rb.withReceiver('opsgenie').addMatcher('team', MatcherOperator.equal, 'operations'))
)
.addReceivers((b) => b.withName('email').addEmailConfig((eb) => eb.withTo('test@example.com')))
.addReceivers((b) => b.withName('slack'))
.addReceivers((b) => b.withName('opsgenie'))
);
} }
function mockTwoAlertManagers() { function mockTwoAlertManagers() {
getAlertManagerDataSourcesByPermissionAndConfigMock.mockReturnValue([ getAlertManagerDataSourcesByPermissionAndConfigMock.mockReturnValue([
{ name: 'OTHER_AM', imgUrl: '', hasConfigurationAPI: true },
grafanaAlertManagerDataSource, grafanaAlertManagerDataSource,
{ name: 'OTHER_AM', imgUrl: '', hasConfigurationAPI: true },
]); ]);
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
amConfigBuilder setAlertmanagerConfig('OTHER_AM', mockConfig);
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('slack').addMatcher('tomato', MatcherOperator.equal, 'red'))
.addRoute((rb) => rb.withReceiver('opsgenie').addMatcher('team', MatcherOperator.equal, 'operations'))
)
.addReceivers((b) => b.withName('email').addEmailConfig((eb) => eb.withTo('test@example.com')))
.addReceivers((b) => b.withName('slack'))
.addReceivers((b) => b.withName('opsgenie'))
);
mockApi(server).getAlertmanagerConfig('OTHER_AM', (amConfigBuilder) =>
amConfigBuilder
.withRoute((routeBuilder) =>
routeBuilder
.withReceiver('email')
.addRoute((rb) => rb.withReceiver('slack').addMatcher('tomato', MatcherOperator.equal, 'red'))
.addRoute((rb) => rb.withReceiver('opsgenie').addMatcher('team', MatcherOperator.equal, 'operations'))
)
.addReceivers((b) => b.withName('email').addEmailConfig((eb) => eb.withTo('test@example.com')))
.addReceivers((b) => b.withName('slack'))
.addReceivers((b) => b.withName('opsgenie'))
);
} }
function mockHasEditPermission(enabled: boolean) { function mockHasEditPermission(enabled: boolean) {
@ -131,14 +114,22 @@ const folder: Folder = {
title: 'title', title: 'title',
}; };
describe('NotificationPreview', () => { describe.each([
// k8s API enabled
true,
// k8s API disabled
false,
])('NotificationPreview with alertingApiServer=%p', (apiServerEnabled) => {
apiServerEnabled ? testWithFeatureToggles(['alertingApiServer']) : testWithFeatureToggles([]);
it('should render notification preview without alert manager label, when having only one alert manager configured to receive alerts', async () => { it('should render notification preview without alert manager label, when having only one alert manager configured to receive alerts', async () => {
mockOneAlertManager(); mockOneAlertManager();
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />); const { user } = render(
<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />
);
await userEvent.click(ui.previewButton.get()); await user.click(ui.previewButton.get());
await waitFor(() => { await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
}); });
@ -160,23 +151,18 @@ describe('NotificationPreview', () => {
mockTwoAlertManagers(); mockTwoAlertManagers();
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />); const { user } = render(
await waitFor(() => { <NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); );
});
await userEvent.click(ui.previewButton.get()); await user.click(await ui.previewButton.find());
await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
});
// we expect the alert manager label to be present as there is more than one alert manager configured to receive alerts // we expect the alert manager label to be present as there is more than one alert manager configured to receive alerts
await waitFor(() => { expect(await ui.grafanaAlertManagerLabel.find()).toBeInTheDocument();
expect(ui.grafanaAlertManagerLabel.query()).toBeInTheDocument(); expect(await ui.otherAlertManagerLabel.find()).toBeInTheDocument();
});
expect(ui.otherAlertManagerLabel.query()).toBeInTheDocument(); const matchingPoliciesElements = await ui.route.findAll();
const matchingPoliciesElements = ui.route.queryAll();
expect(matchingPoliciesElements).toHaveLength(2); expect(matchingPoliciesElements).toHaveLength(2);
expect(matchingPoliciesElements[0]).toHaveTextContent(/tomato = red/); expect(matchingPoliciesElements[0]).toHaveTextContent(/tomato = red/);
expect(matchingPoliciesElements[1]).toHaveTextContent(/tomato = red/); expect(matchingPoliciesElements[1]).toHaveTextContent(/tomato = red/);
@ -187,13 +173,15 @@ describe('NotificationPreview', () => {
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
mockHasEditPermission(true); mockHasEditPermission(true);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />); const { user } = render(
<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />
);
await waitFor(() => { await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
}); });
await userEvent.click(ui.previewButton.get()); await user.click(ui.previewButton.get());
await userEvent.click(await ui.seeDetails.find()); await user.click(await ui.seeDetails.find());
expect(ui.details.title.query()).toBeInTheDocument(); expect(ui.details.title.query()).toBeInTheDocument();
//we expect seeing the default policy //we expect seeing the default policy
expect(screen.getByText(/default policy/i)).toBeInTheDocument(); expect(screen.getByText(/default policy/i)).toBeInTheDocument();
@ -209,13 +197,15 @@ describe('NotificationPreview', () => {
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]); mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
mockHasEditPermission(false); mockHasEditPermission(false);
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />); const { user } = render(
<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />
);
await waitFor(() => { await waitFor(() => {
expect(ui.loadingIndicator.query()).not.toBeInTheDocument(); expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
}); });
await userEvent.click(ui.previewButton.get()); await user.click(ui.previewButton.get());
await userEvent.click(await ui.seeDetails.find()); await user.click(await ui.seeDetails.find());
expect(ui.details.title.query()).toBeInTheDocument(); expect(ui.details.title.query()).toBeInTheDocument();
//we expect seeing the default policy //we expect seeing the default policy
expect(screen.getByText(/default policy/i)).toBeInTheDocument(); expect(screen.getByText(/default policy/i)).toBeInTheDocument();
@ -234,7 +224,7 @@ describe('NotificationPreviewByAlertmanager', () => {
{ job: 'prometheus', severity: 'warning' }, { job: 'prometheus', severity: 'warning' },
]; ];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
routeBuilder routeBuilder
@ -246,10 +236,9 @@ describe('NotificationPreviewByAlertmanager', () => {
.addReceivers((b) => b.withName('slack')) .addReceivers((b) => b.withName('slack'))
.addReceivers((b) => b.withName('opsgenie')) .addReceivers((b) => b.withName('opsgenie'))
); );
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
const user = userEvent.setup(); const { user } = render(
render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances} potentialInstances={potentialInstances}
@ -285,7 +274,7 @@ describe('NotificationPreviewByAlertmanager', () => {
{ job: 'prometheus', severity: 'warning' }, { job: 'prometheus', severity: 'warning' },
]; ];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
routeBuilder routeBuilder
@ -300,10 +289,9 @@ describe('NotificationPreviewByAlertmanager', () => {
.addReceivers((b) => b.withName('slack')) .addReceivers((b) => b.withName('slack'))
.addReceivers((b) => b.withName('opsgenie')) .addReceivers((b) => b.withName('opsgenie'))
); );
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
const user = userEvent.setup(); const { user } = render(
render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances} potentialInstances={potentialInstances}
@ -339,7 +327,7 @@ describe('NotificationPreviewByAlertmanager', () => {
{ job: 'prometheus', severity: 'warning' }, { job: 'prometheus', severity: 'warning' },
]; ];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
routeBuilder routeBuilder
@ -355,9 +343,9 @@ describe('NotificationPreviewByAlertmanager', () => {
.addReceivers((b) => b.withName('opsgenie')) .addReceivers((b) => b.withName('opsgenie'))
); );
const user = userEvent.setup(); setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
render( const { user } = render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
potentialInstances={potentialInstances} potentialInstances={potentialInstances}
@ -392,7 +380,7 @@ describe('NotificationPreviewByAlertmanager', () => {
it('does not match regex in middle of the word as alertmanager will anchor when queried via API', async () => { it('does not match regex in middle of the word as alertmanager will anchor when queried via API', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'foobarfoo' }]; const potentialInstances: Labels[] = [{ regexfield: 'foobarfoo' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.addReceivers((b) => b.withName('email')) .addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
@ -402,6 +390,8 @@ describe('NotificationPreviewByAlertmanager', () => {
) )
); );
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
render( render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager
alertManagerSource={grafanaAlertManagerDataSource} alertManagerSource={grafanaAlertManagerDataSource}
@ -417,7 +407,7 @@ describe('NotificationPreviewByAlertmanager', () => {
it('matches regex at the start of the word', async () => { it('matches regex at the start of the word', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'baaaaaaah' }]; const potentialInstances: Labels[] = [{ regexfield: 'baaaaaaah' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.addReceivers((b) => b.withName('email')) .addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
@ -426,6 +416,7 @@ describe('NotificationPreviewByAlertmanager', () => {
.addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, 'ba.*h')) .addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, 'ba.*h'))
) )
); );
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
render( render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager
@ -441,7 +432,7 @@ describe('NotificationPreviewByAlertmanager', () => {
it('handles negated regex correctly', async () => { it('handles negated regex correctly', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'thing' }]; const potentialInstances: Labels[] = [{ regexfield: 'thing' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.addReceivers((b) => b.withName('email')) .addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
@ -450,6 +441,7 @@ describe('NotificationPreviewByAlertmanager', () => {
.addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.notRegex, 'thing')) .addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.notRegex, 'thing'))
) )
); );
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
render( render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager
@ -466,7 +458,7 @@ describe('NotificationPreviewByAlertmanager', () => {
it('matches regex with flags', async () => { it('matches regex with flags', async () => {
const potentialInstances: Labels[] = [{ regexfield: 'baaaaaaah' }]; const potentialInstances: Labels[] = [{ regexfield: 'baaaaaaah' }];
mockApi(server).getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, (amConfigBuilder) => const mockConfig = getMockConfig((amConfigBuilder) =>
amConfigBuilder amConfigBuilder
.addReceivers((b) => b.withName('email')) .addReceivers((b) => b.withName('email'))
.withRoute((routeBuilder) => .withRoute((routeBuilder) =>
@ -475,6 +467,7 @@ describe('NotificationPreviewByAlertmanager', () => {
.addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, '(?i)BA.*h')) .addRoute((rb) => rb.withReceiver('email').addMatcher('regexfield', MatcherOperator.regex, '(?i)BA.*h'))
) )
); );
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, mockConfig);
render( render(
<NotificationPreviewByAlertManager <NotificationPreviewByAlertManager

@ -2,6 +2,7 @@ import { compact } from 'lodash';
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { Button, LoadingPlaceholder, Stack, Text } from '@grafana/ui'; import { Button, LoadingPlaceholder, Stack, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi'; import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertQuery } from 'app/types/unified-alerting-dto';
@ -64,26 +65,32 @@ export const NotificationPreview = ({
<Stack direction="column"> <Stack direction="column">
<Stack direction="row" alignItems="flex-start" justifyContent="space-between"> <Stack direction="row" alignItems="flex-start" justifyContent="space-between">
<Stack direction="column" gap={1}> <Stack direction="column" gap={1}>
<Text element="h5">Alert instance routing preview</Text> <Text element="h5">
<Trans i18nKey="alerting.notification-preview.title">Alert instance routing preview</Trans>
</Text>
{isLoading && previewUninitialized && ( {isLoading && previewUninitialized && (
<Text color="secondary" variant="bodySmall"> <Text color="secondary" variant="bodySmall">
Loading... <Trans i18nKey="alerting.common.loading">Loading...</Trans>
</Text> </Text>
)} )}
{previewUninitialized ? ( {previewUninitialized ? (
<Text color="secondary" variant="bodySmall"> <Text color="secondary" variant="bodySmall">
When you have your folder selected and your query and labels are configured, click &quot;Preview <Trans i18nKey="alerting.notification-preview.uninitialized">
routing&quot; to see the results here. When you have your folder selected and your query and labels are configured, click &quot;Preview
routing&quot; to see the results here.
</Trans>
</Text> </Text>
) : ( ) : (
<Text color="secondary" variant="bodySmall"> <Text color="secondary" variant="bodySmall">
Based on the labels added, alert instances are routed to the following notification policies. Expand each <Trans i18nKey="alerting.notification-preview.initialized">
notification policy below to view more details. Based on the labels added, alert instances are routed to the following notification policies. Expand
each notification policy below to view more details.
</Trans>
</Text> </Text>
)} )}
</Stack> </Stack>
<Button icon="sync" variant="secondary" type="button" onClick={onPreview} disabled={disabled}> <Button icon="sync" variant="secondary" type="button" onClick={onPreview} disabled={disabled}>
Preview routing <Trans i18nKey="alerting.notification-preview.preview-routing">Preview routing</Trans>
</Button> </Button>
</Stack> </Stack>
{!isLoading && !previewUninitialized && potentialInstances.length > 0 && ( {!isLoading && !previewUninitialized && potentialInstances.length > 0 && (

@ -2,6 +2,8 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
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';
@ -27,9 +29,12 @@ function NotificationPreviewByAlertManager({
); );
if (error) { if (error) {
const title = t('alerting.notification-preview.error', 'Could not load routing preview for {{alertmanager}}', {
alertmanager: alertManagerSource.name,
});
return ( return (
<Alert title="Cannot load Alertmanager configuration" severity="error"> <Alert title={title} severity="error">
{error.message} {stringifyErrorLike(error)}
</Alert> </Alert>
); );
} }
@ -46,8 +51,7 @@ function NotificationPreviewByAlertManager({
<Stack direction="row" alignItems="center"> <Stack direction="row" alignItems="center">
<div className={styles.firstAlertManagerLine}></div> <div className={styles.firstAlertManagerLine}></div>
<div className={styles.alertManagerName}> <div className={styles.alertManagerName}>
{' '} <Trans i18nKey="alerting.notification-preview.alertmanager">Alertmanager:</Trans>
Alertmanager:
<img src={alertManagerSource.imgUrl} alt="" className={styles.img} /> <img src={alertManagerSource.imgUrl} alt="" className={styles.img} />
{alertManagerSource.name} {alertManagerSource.name}
</div> </div>

@ -1,9 +1,11 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { useContactPointsWithStatus } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { useNotificationPolicyRoute } from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute';
import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
import { Labels } from '../../../../../../types/unified-alerting-dto'; import { Labels } from '../../../../../../types/unified-alerting-dto';
import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig';
import { useRouteGroupsMatcher } from '../../../useRouteGroupsMatcher'; import { useRouteGroupsMatcher } from '../../../useRouteGroupsMatcher';
import { addUniqueIdentifierToRoute } from '../../../utils/amroutes'; import { addUniqueIdentifierToRoute } from '../../../utils/amroutes';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
@ -11,41 +13,50 @@ import { AlertInstanceMatch, computeInheritedTree, normalizeRoute } from '../../
import { getRoutesByIdMap, RouteWithPath } from './route'; import { getRoutesByIdMap, RouteWithPath } from './route';
export const useAlertmanagerNotificationRoutingPreview = ( export const useAlertmanagerNotificationRoutingPreview = (alertmanager: string, potentialInstances: Labels[]) => {
alertManagerSourceName: string, const {
potentialInstances: Labels[] data: currentData,
) => { isLoading: isPoliciesLoading,
const { currentData, isLoading: configLoading, error: configError } = useAlertmanagerConfig(alertManagerSourceName); error: policiesError,
const config = currentData?.alertmanager_config; } = useNotificationPolicyRoute({ alertmanager });
const {
contactPoints,
isLoading: contactPointsLoading,
error: contactPointsError,
} = useContactPointsWithStatus({
alertmanager,
fetchPolicies: false,
fetchStatuses: false,
});
const { matchInstancesToRoute } = useRouteGroupsMatcher(); const { matchInstancesToRoute } = useRouteGroupsMatcher();
// to create the list of matching contact points we need to first get the rootRoute const [defaultPolicy] = currentData ?? [];
const { rootRoute, receivers } = useMemo(() => { const rootRoute = useMemo(() => {
if (!config) { if (!defaultPolicy) {
return { return;
receivers: [],
rootRoute: undefined,
};
} }
return normalizeRoute(addUniqueIdentifierToRoute(defaultPolicy));
return { }, [defaultPolicy]);
rootRoute: config.route ? normalizeRoute(addUniqueIdentifierToRoute(config.route)) : undefined,
receivers: config.receivers ?? [],
};
}, [config]);
// create maps for routes to be get by id, this map also contains the path to the route // create maps for routes to be get by id, this map also contains the path to the route
// ⚠ don't forget to compute the inherited tree before using this map // ⚠ don't forget to compute the inherited tree before using this map
const routesByIdMap: Map<string, RouteWithPath> = rootRoute const routesByIdMap = rootRoute
? getRoutesByIdMap(computeInheritedTree(rootRoute)) ? getRoutesByIdMap(computeInheritedTree(rootRoute))
: new Map(); : new Map<string, RouteWithPath>();
// create map for receivers to be get by name // to create the list of matching contact points we need to first get the rootRoute
const receiversByName = const receiversByName = useMemo(() => {
receivers.reduce((map, receiver) => { if (!contactPoints) {
return new Map<string, Receiver>();
}
// create map for receivers to be get by name
return contactPoints.reduce((map, receiver) => {
return map.set(receiver.name, receiver); return map.set(receiver.name, receiver);
}, new Map<string, Receiver>()) ?? new Map<string, Receiver>(); }, new Map<string, Receiver>());
}, [contactPoints]);
// match labels in the tree => map of notification policies and the alert instances (list of labels) in each one // match labels in the tree => map of notification policies and the alert instances (list of labels) in each one
const { const {
@ -56,8 +67,9 @@ export const useAlertmanagerNotificationRoutingPreview = (
if (!rootRoute) { if (!rootRoute) {
return; return;
} }
return await matchInstancesToRoute(rootRoute, potentialInstances, { return await matchInstancesToRoute(rootRoute, potentialInstances, {
unquoteMatchers: alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME, unquoteMatchers: alertmanager !== GRAFANA_RULES_SOURCE_NAME,
}); });
}, [rootRoute, potentialInstances]); }, [rootRoute, potentialInstances]);
@ -65,7 +77,7 @@ export const useAlertmanagerNotificationRoutingPreview = (
routesByIdMap, routesByIdMap,
receiversByName, receiversByName,
matchingMap, matchingMap,
loading: configLoading || matchingLoading, loading: isPoliciesLoading || contactPointsLoading || matchingLoading,
error: configError ?? matchingError, error: policiesError ?? contactPointsError ?? matchingError,
}; };
}; };

@ -159,23 +159,11 @@ export class AlertmanagerReceiverBuilder {
} }
} }
export function mockApi(server: SetupServer) { export const getMockConfig = (configure: (builder: AlertmanagerConfigBuilder) => void): AlertManagerCortexConfig => {
return { const builder = new AlertmanagerConfigBuilder();
getAlertmanagerConfig: (amName: string, configure: (builder: AlertmanagerConfigBuilder) => void) => { configure(builder);
const builder = new AlertmanagerConfigBuilder(); return { alertmanager_config: builder.build(), template_files: {} };
configure(builder); };
server.use(
http.get(`api/alertmanager/${amName}/config/api/v1/alerts`, () =>
HttpResponse.json<AlertManagerCortexConfig>({
alertmanager_config: builder.build(),
template_files: {},
})
)
);
},
};
}
export function mockAlertRuleApi(server: SetupServer) { export function mockAlertRuleApi(server: SetupServer) {
return { return {

@ -14,9 +14,8 @@ const normalizeMatchers = (route: Route) => {
const routeMatchers: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Matcher[] = []; const routeMatchers: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Matcher[] = [];
if (route.object_matchers) { if (route.object_matchers) {
// todo foreach route.object_matchers.forEach(([label, type, value]) => {
route.object_matchers.map(([label, type, value]) => { routeMatchers.push({ label, type, value });
return { label, type, value };
}); });
} }

@ -7,41 +7,43 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } f
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { PROVENANCE_NONE, K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; import { PROVENANCE_NONE, K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); const getReceiversList = () => {
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
// Turn our mock alertmanager config into the format that we expect to be returned by the k8s API // Turn our mock alertmanager config into the format that we expect to be returned by the k8s API
const mappedReceivers = const mappedReceivers =
config.alertmanager_config?.receivers?.map((contactPoint) => { config.alertmanager_config?.receivers?.map((contactPoint) => {
const provenance = const provenance =
contactPoint.grafana_managed_receiver_configs?.find((integration) => { contactPoint.grafana_managed_receiver_configs?.find((integration) => {
return integration.provenance; return integration.provenance;
})?.provenance || PROVENANCE_NONE; })?.provenance || PROVENANCE_NONE;
return { return {
metadata: { metadata: {
// This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses // This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses
uid: camelCase(contactPoint.name), uid: camelCase(contactPoint.name),
annotations: { annotations: {
[K8sAnnotations.Provenance]: provenance, [K8sAnnotations.Provenance]: provenance,
[K8sAnnotations.AccessAdmin]: 'true', [K8sAnnotations.AccessAdmin]: 'true',
[K8sAnnotations.AccessDelete]: 'true', [K8sAnnotations.AccessDelete]: 'true',
[K8sAnnotations.AccessWrite]: 'true', [K8sAnnotations.AccessWrite]: 'true',
},
}, },
}, spec: {
spec: { title: contactPoint.name,
title: contactPoint.name, integrations: contactPoint.grafana_managed_receiver_configs || [],
integrations: contactPoint.grafana_managed_receiver_configs || [], },
}, };
}; }) || [];
}) || [];
const parsedReceivers = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver>( return getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver>(
'ReceiverList', 'ReceiverList',
mappedReceivers mappedReceivers
); );
};
const listNamespacedReceiverHandler = () => const listNamespacedReceiverHandler = () =>
http.get<{ namespace: string }>(`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers`, () => { http.get<{ namespace: string }>(`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers`, () => {
return HttpResponse.json(parsedReceivers); return HttpResponse.json(getReceiversList());
}); });
const createNamespacedReceiverHandler = () => const createNamespacedReceiverHandler = () =>
@ -58,6 +60,7 @@ const deleteNamespacedReceiverHandler = () =>
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`, `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`,
({ params }) => { ({ params }) => {
const { name } = params; const { name } = params;
const parsedReceivers = getReceiversList();
const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name); const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name);
if (matchedReceiver) { if (matchedReceiver) {
return HttpResponse.json(parsedReceivers); return HttpResponse.json(parsedReceivers);

@ -3,15 +3,19 @@ import { HttpResponse, http } from 'msw';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
const alertmanagerConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); const getNotificationReceiversHandler = () =>
const defaultReceiversResponse = alertmanagerConfig.alertmanager_config.receivers; http.get('/api/v1/notifications/receivers', () => {
const defaultTimeIntervalsResponse = alertmanagerConfig.alertmanager_config.time_intervals; const receivers = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME).alertmanager_config.receivers || [];
const getNotificationReceiversHandler = (response = defaultReceiversResponse) => return HttpResponse.json(receivers);
http.get('/api/v1/notifications/receivers', () => HttpResponse.json(response)); });
const getTimeIntervalsHandler = (response = defaultTimeIntervalsResponse) => const getTimeIntervalsHandler = () =>
http.get('/api/v1/notifications/time-intervals', () => HttpResponse.json(response)); http.get('/api/v1/notifications/time-intervals', () => {
const intervals = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME).alertmanager_config.time_intervals;
return HttpResponse.json(intervals);
});
const handlers = [getNotificationReceiversHandler(), getTimeIntervalsHandler()]; const handlers = [getNotificationReceiversHandler(), getTimeIntervalsHandler()];

@ -245,6 +245,7 @@
"edit": "Edit", "edit": "Edit",
"export": "Export", "export": "Export",
"export-all": "Export all", "export-all": "Export all",
"loading": "Loading...",
"view": "View" "view": "View"
}, },
"contact-points": { "contact-points": {
@ -329,6 +330,14 @@
"save": "Save mute timing", "save": "Save mute timing",
"saving": "Saving mute timing" "saving": "Saving mute timing"
}, },
"notification-preview": {
"alertmanager": "Alertmanager:",
"error": "Could not load routing preview for {{alertmanager}}",
"initialized": "Based on the labels added, alert instances are routed to the following notification policies. Expand each notification policy below to view more details.",
"preview-routing": "Preview routing",
"title": "Alert instance routing preview",
"uninitialized": "When you have your folder selected and your query and labels are configured, click \"Preview routing\" to see the results here."
},
"policies": { "policies": {
"default-policy": { "default-policy": {
"description": "All alert instances will be handled by the default policy if no other matching policies are found.", "description": "All alert instances will be handled by the default policy if no other matching policies are found.",

@ -245,6 +245,7 @@
"edit": "Ēđįŧ", "edit": "Ēđįŧ",
"export": "Ēχpőřŧ", "export": "Ēχpőřŧ",
"export-all": "Ēχpőřŧ äľľ", "export-all": "Ēχpőřŧ äľľ",
"loading": "Ŀőäđįʼnģ...",
"view": "Vįęŵ" "view": "Vįęŵ"
}, },
"contact-points": { "contact-points": {
@ -329,6 +330,14 @@
"save": "Ŝävę mūŧę ŧįmįʼnģ", "save": "Ŝävę mūŧę ŧįmįʼnģ",
"saving": "Ŝävįʼnģ mūŧę ŧįmįʼnģ" "saving": "Ŝävįʼnģ mūŧę ŧįmįʼnģ"
}, },
"notification-preview": {
"alertmanager": "Åľęřŧmäʼnäģęř:",
"error": "Cőūľđ ʼnőŧ ľőäđ řőūŧįʼnģ přęvįęŵ ƒőř {{alertmanager}}",
"initialized": "ßäşęđ őʼn ŧĥę ľäþęľş äđđęđ, äľęřŧ įʼnşŧäʼnčęş äřę řőūŧęđ ŧő ŧĥę ƒőľľőŵįʼnģ ʼnőŧįƒįčäŧįőʼn pőľįčįęş. Ēχpäʼnđ ęäčĥ ʼnőŧįƒįčäŧįőʼn pőľįčy þęľőŵ ŧő vįęŵ mőřę đęŧäįľş.",
"preview-routing": "Přęvįęŵ řőūŧįʼnģ",
"title": "Åľęřŧ įʼnşŧäʼnčę řőūŧįʼnģ přęvįęŵ",
"uninitialized": "Ŵĥęʼn yőū ĥävę yőūř ƒőľđęř şęľęčŧęđ äʼnđ yőūř qūęřy äʼnđ ľäþęľş äřę čőʼnƒįģūřęđ, čľįčĸ \"Přęvįęŵ řőūŧįʼnģ\" ŧő şęę ŧĥę řęşūľŧş ĥęřę."
},
"policies": { "policies": {
"default-policy": { "default-policy": {
"description": "Åľľ äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę ĥäʼnđľęđ þy ŧĥę đęƒäūľŧ pőľįčy įƒ ʼnő őŧĥęř mäŧčĥįʼnģ pőľįčįęş äřę ƒőūʼnđ.", "description": "Åľľ äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę ĥäʼnđľęđ þy ŧĥę đęƒäūľŧ pőľįčy įƒ ʼnő őŧĥęř mäŧčĥįʼnģ pőľįčįęş äřę ƒőūʼnđ.",

Loading…
Cancel
Save