Alerting: Remove old contact points view (#78704)

This commit is contained in:
Gilles De Mey
2023-11-30 13:37:14 +01:00
committed by GitHub
parent 24082d61c2
commit 7cbf5ae78d
37 changed files with 627 additions and 1885 deletions

View File

@@ -1830,7 +1830,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx:5381": [
"public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/components/export/FileExportPreview.tsx:5381": [

View File

@@ -74,7 +74,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `splitScopes` | Support faster dashboard and folder search by splitting permission scopes into parts |
| `dashgpt` | Enable AI powered features in dashboards |
| `reportingRetries` | Enables rendering retries for the reporting feature |
| `alertingContactPointsV2` | Show the new contacpoints list view |
| `transformationsVariableSupport` | Allows using variables in transformations |
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |

View File

@@ -124,7 +124,6 @@ export interface FeatureToggles {
lokiRunQueriesInParallel?: boolean;
wargamesTesting?: boolean;
alertingInsights?: boolean;
alertingContactPointsV2?: boolean;
externalCorePlugins?: boolean;
pluginsAPIMetrics?: boolean;
httpSLOLevels?: boolean;

View File

@@ -791,13 +791,6 @@ var (
AllowSelfServe: falsePtr,
HideFromAdminPage: true, // This is moving away from being a feature toggle.
},
{
Name: "alertingContactPointsV2",
Description: "Show the new contacpoints list view",
FrontendOnly: true,
Stage: FeatureStagePublicPreview,
Owner: grafanaAlertingSquad,
},
{
Name: "externalCorePlugins",
Description: "Allow core plugins to be loaded as external",

View File

@@ -105,7 +105,6 @@ libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,false,true,false
lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false,false
wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false,false
alertingInsights,GA,@grafana/alerting-squad,false,false,false,true
alertingContactPointsV2,preview,@grafana/alerting-squad,false,false,false,true
externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false,false
pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,false,true
httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
105 lokiRunQueriesInParallel privatePreview @grafana/observability-logs false false false false
106 wargamesTesting experimental @grafana/hosted-grafana-team false false false false
107 alertingInsights GA @grafana/alerting-squad false false false true
alertingContactPointsV2 preview @grafana/alerting-squad false false false true
108 externalCorePlugins experimental @grafana/plugins-platform-backend false false false false
109 pluginsAPIMetrics experimental @grafana/plugins-platform-backend false false false true
110 httpSLOLevels experimental @grafana/hosted-grafana-team false false true false

View File

@@ -431,10 +431,6 @@ const (
// Show the new alerting insights landing page
FlagAlertingInsights = "alertingInsights"
// FlagAlertingContactPointsV2
// Show the new contacpoints list view
FlagAlertingContactPointsV2 = "alertingContactPointsV2"
// FlagExternalCorePlugins
// Allow core plugins to be loaded as external
FlagExternalCorePlugins = "externalCorePlugins"

View File

@@ -1,15 +1,13 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { config } from '@grafana/runtime';
import { withErrorBoundary } from '@grafana/ui';
const ContactPointsV1 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v1'));
const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints.v2'));
const ContactPointsV2 = SafeDynamicImport(() => import('./components/contact-points/ContactPoints'));
const EditContactPoint = SafeDynamicImport(() => import('./components/contact-points/EditContactPoint'));
const NewContactPoint = SafeDynamicImport(() => import('./components/contact-points/NewContactPoint'));
const EditMessageTemplate = SafeDynamicImport(() => import('./components/contact-points/EditMessageTemplate'));
const NewMessageTemplate = SafeDynamicImport(() => import('./components/contact-points/NewMessageTemplate'));
const GlobalConfig = SafeDynamicImport(() => import('./components/contact-points/GlobalConfig'));
const GlobalConfig = SafeDynamicImport(() => import('./components/contact-points/components/GlobalConfig'));
const DuplicateMessageTemplate = SafeDynamicImport(
() => import('./components/contact-points/DuplicateMessageTemplate')
);
@@ -18,29 +16,21 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
const newContactPointsListView = config.featureToggles.alertingContactPointsV2 ?? false;
// TODO add pagenav back in that way we have correct breadcrumbs and page title
const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => (
const ContactPoints = (_props: GrafanaRouteComponentProps): JSX.Element => (
<AlertmanagerPageWrapper pageId="receivers" accessType="notification">
{/* TODO do we want a "routes" component for each Alerting entity? */}
{newContactPointsListView ? (
<Switch>
<Route exact={true} path="/alerting/notifications" component={ContactPointsV2} />
<Route exact={true} path="/alerting/notifications/receivers/new" component={NewContactPoint} />
<Route exact={true} path="/alerting/notifications/receivers/:name/edit" component={EditContactPoint} />
<Route exact={true} path="/alerting/notifications/templates/:name/edit" component={EditMessageTemplate} />
<Route exact={true} path="/alerting/notifications/templates/new" component={NewMessageTemplate} />
<Route
exact={true}
path="/alerting/notifications/templates/:name/duplicate"
component={DuplicateMessageTemplate}
/>
<Route exact={true} path="/alerting/notifications/global-config" component={GlobalConfig} />
</Switch>
) : (
<ContactPointsV1 {...props} />
)}
<Switch>
<Route exact={true} path="/alerting/notifications" component={ContactPointsV2} />
<Route exact={true} path="/alerting/notifications/receivers/new" component={NewContactPoint} />
<Route exact={true} path="/alerting/notifications/receivers/:name/edit" component={EditContactPoint} />
<Route exact={true} path="/alerting/notifications/templates/:name/edit" component={EditMessageTemplate} />
<Route exact={true} path="/alerting/notifications/templates/new" component={NewMessageTemplate} />
<Route
exact={true}
path="/alerting/notifications/templates/:name/duplicate"
component={DuplicateMessageTemplate}
/>
<Route exact={true} path="/alerting/notifications/global-config" component={GlobalConfig} />
</Switch>
</AlertmanagerPageWrapper>
);

View File

@@ -5,6 +5,7 @@ import React, { PropsWithChildren } from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { selectors } from '@grafana/e2e-selectors';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
@@ -13,9 +14,12 @@ import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { setupDataSources } from '../../testSetup/datasources';
import { DataSourceType } from '../../utils/datasource';
import ContactPoints, { ContactPoint } from './ContactPoints.v2';
import ContactPoints, { ContactPoint } from './ContactPoints';
import setupGrafanaManagedServer from './__mocks__/grafanaManagedServer';
import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimirFlavoredServer';
import setupVanillaAlertmanagerFlavoredServer, {
VANILLA_ALERTMANAGER_DATASOURCE_UID,
} from './__mocks__/vanillaAlertmanagerServer';
/**
* There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them.
@@ -38,17 +42,15 @@ const server = setupMswServer();
describe('contact points', () => {
describe('Contact points with Grafana managed alertmanager', () => {
beforeEach(() => {
setupGrafanaManagedServer(server);
});
beforeAll(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
]);
setupGrafanaManagedServer(server);
});
it('should show / hide loading states', async () => {
it('should show / hide loading states, have all actions enabled', async () => {
render(
<AlertmanagerProvider accessType={'notification'}>
<ContactPoints />
@@ -64,6 +66,67 @@ describe('contact points', () => {
expect(screen.getByText('grafana-default-email')).toBeInTheDocument();
expect(screen.getAllByTestId('contact-point')).toHaveLength(4);
// check for available actions our mock 4 contact points, 1 of them is provisioned
expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'export all' })).toBeInTheDocument();
// 2 of them are unused by routes in the mock response
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(2);
const viewProvisioned = screen.getByRole('link', { name: 'view-action' });
expect(viewProvisioned).toBeInTheDocument();
expect(viewProvisioned).not.toBeDisabled();
const editButtons = screen.getAllByRole('link', { name: 'edit-action' });
expect(editButtons).toHaveLength(3);
editButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
const moreActionsButtons = screen.getAllByRole('button', { name: 'more-actions' });
expect(moreActionsButtons).toHaveLength(4);
moreActionsButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
});
it('should disable certain actions if the user has no write permissions', async () => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
render(
<AlertmanagerProvider accessType={'notification'}>
<ContactPoints />
</AlertmanagerProvider>,
{ wrapper: TestProvider }
);
// wait for loading to be done
await waitFor(async () => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// should disable create contact point
expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true');
// there should be no edit buttons
expect(screen.queryAllByRole('link', { name: 'edit-action' })).toHaveLength(0);
// there should be view buttons though
const viewButtons = screen.getAllByRole('link', { name: 'view-action' });
expect(viewButtons).toHaveLength(4);
// delete should be disabled in the "more" actions
const moreButtons = screen.queryAllByRole('button', { name: 'more-actions' });
expect(moreButtons).toHaveLength(4);
// check if all of the delete buttons are disabled
for await (const button of moreButtons) {
await userEvent.click(button);
const deleteButton = await screen.queryByRole('menuitem', { name: 'delete' });
expect(deleteButton).toBeDisabled();
}
});
it('should call delete when clicked and not disabled', async () => {
@@ -175,7 +238,7 @@ describe('contact points', () => {
);
});
it('should show / hide loading states', async () => {
it('should show / hide loading states, have the right actions enabled', async () => {
render(
<TestProvider>
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={MIMIR_DATASOURCE_UID}>
@@ -193,6 +256,76 @@ describe('contact points', () => {
expect(screen.getByText('mixed')).toBeInTheDocument();
expect(screen.getByText('some webhook')).toBeInTheDocument();
expect(screen.getAllByTestId('contact-point')).toHaveLength(2);
// check for available actions export should be disabled
expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'export all' })).not.toBeInTheDocument();
// 1 of them is used by a route in the mock response
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(1);
const editButtons = screen.getAllByRole('link', { name: 'edit-action' });
expect(editButtons).toHaveLength(2);
editButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
const moreActionsButtons = screen.getAllByRole('button', { name: 'more-actions' });
expect(moreActionsButtons).toHaveLength(2);
moreActionsButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
});
});
describe('Vanilla Alertmanager ', () => {
beforeEach(() => {
setupVanillaAlertmanagerFlavoredServer(server);
});
beforeAll(() => {
grantUserPermissions([
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
const alertManager = mockDataSource<AlertManagerDataSourceJsonData>({
name: VANILLA_ALERTMANAGER_DATASOURCE_UID,
uid: VANILLA_ALERTMANAGER_DATASOURCE_UID,
type: DataSourceType.Alertmanager,
jsonData: {
implementation: AlertManagerImplementation.prometheus,
handleGrafanaManagedAlerts: true,
},
});
setupDataSources(alertManager);
});
it("should not allow any editing because it's not supported", async () => {
render(
<TestProvider>
<AlertmanagerProvider
accessType={'notification'}
alertmanagerSourceName={VANILLA_ALERTMANAGER_DATASOURCE_UID}
>
<ContactPoints />
</AlertmanagerProvider>
</TestProvider>
);
await waitFor(async () => {
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitForElementToBeRemoved(screen.getByText('Loading...'));
expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument();
});
expect(screen.queryByRole('link', { name: 'add contact point' })).not.toBeInTheDocument();
const viewProvisioned = screen.getByRole('link', { name: 'view-action' });
expect(viewProvisioned).toBeInTheDocument();
expect(viewProvisioned).not.toBeDisabled();
});
});
});

View File

@@ -45,14 +45,14 @@ import { Spacer } from '../Spacer';
import { Strong } from '../Strong';
import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter';
import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter';
import { GlobalConfigAlert } from '../receivers/ReceiversAndTemplatesView';
import { UnusedContactPointBadge } from '../receivers/ReceiversTable';
import { ReceiverMetadataBadge } from '../receivers/grafanaAppReceivers/ReceiverMetadataBadge';
import { ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata';
import { ContactPointsFilter } from './ContactPointsFilter';
import { useDeleteContactPointModal } from './Modals';
import { NotificationTemplates } from './NotificationTemplates';
import { ContactPointsFilter } from './components/ContactPointsFilter';
import { GlobalConfigAlert } from './components/GlobalConfigAlert';
import { useDeleteContactPointModal } from './components/Modals';
import { UnusedContactPointBadge } from './components/UnusedBadge';
import {
RECEIVER_META_KEY,
RECEIVER_PLUGIN_META_KEY,
@@ -135,6 +135,7 @@ const ContactPoints = () => {
{addContactPointSupported && (
<LinkButton
icon="plus"
aria-label="add contact point"
variant="primary"
href="/alerting/notifications/receivers/new"
disabled={!addContactPointAllowed}
@@ -146,6 +147,7 @@ const ContactPoints = () => {
<Button
icon="download-alt"
variant="secondary"
aria-label="export all"
disabled={!exportContactPointsAllowed}
onClick={() => showExportDrawer(ALL_CONTACT_POINTS)}
>
@@ -364,6 +366,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => {
<Menu.Item
icon="download-alt"
label="Export"
ariaLabel="export"
disabled={!exportAllowed}
data-testid="export"
onClick={() => openExportDrawer(name)}
@@ -386,6 +389,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => {
>
<Menu.Item
label="Delete"
ariaLabel="delete"
icon="trash-alt"
destructive
disabled={disabled || !canDelete}

View File

@@ -1,699 +0,0 @@
import { render, waitFor, within, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
import { contextSrv } from 'app/core/services/context_srv';
import store from 'app/core/store';
import {
AlertmanagerChoice,
AlertManagerDataSourceJsonData,
AlertManagerImplementation,
} from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction, ContactPointsState } from 'app/types';
import 'whatwg-fetch';
import 'core-js/stable/structured-clone';
import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from '../../api/alertmanager';
import { AlertmanagersChoiceResponse } from '../../api/alertmanagerApi';
import { discoverAlertmanagerFeatures } from '../../api/buildInfo';
import { fetchNotifiers } from '../../api/grafana';
import * as receiversApi from '../../api/receiversApi';
import * as grafanaApp from '../../components/receivers/grafanaAppReceivers/grafanaApp';
import { mockApi, setupMswServer } from '../../mockApi';
import {
grantUserPermissions,
mockDataSource,
MockDataSourceSrv,
onCallPluginMetaMock,
someCloudAlertManagerConfig,
someCloudAlertManagerStatus,
someGrafanaAlertManagerConfig,
} from '../../mocks';
import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
import { grafanaNotifiersMock } from '../../mocks/grafana-notifiers';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { getAllDataSources } from '../../utils/config';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../../utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import Receivers from './ContactPoints.v1';
jest.mock('../../api/alertmanager');
jest.mock('../../api/grafana');
jest.mock('../../utils/config');
jest.mock('app/core/services/context_srv');
jest.mock('../../api/buildInfo');
const mocks = {
getAllDataSources: jest.mocked(getAllDataSources),
api: {
fetchConfig: jest.mocked(fetchAlertManagerConfig),
fetchStatus: jest.mocked(fetchStatus),
updateConfig: jest.mocked(updateAlertManagerConfig),
fetchNotifiers: jest.mocked(fetchNotifiers),
testReceivers: jest.mocked(testReceivers),
discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures),
},
hooks: {
useGetContactPointsState: jest.spyOn(receiversApi, 'useGetContactPointsState'),
},
contextSrv: jest.mocked(contextSrv),
};
const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = {
alertmanagersChoice: AlertmanagerChoice.Internal,
numExternalAlertmanagers: 0,
};
const dataSources = {
alertManager: mockDataSource({
name: 'CloudManager',
type: DataSourceType.Alertmanager,
}),
promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({
name: 'PromManager',
type: DataSourceType.Alertmanager,
jsonData: {
implementation: AlertManagerImplementation.prometheus,
},
}),
};
const renderReceivers = (alertManagerSourceName?: string) => {
locationService.push('/alerting/notifications');
return render(
<TestProvider>
<AlertmanagerProvider accessType="notification" alertmanagerSourceName={alertManagerSourceName}>
<Receivers />
</AlertmanagerProvider>
</TestProvider>
);
};
const ui = {
newContactPointButton: byRole('link', { name: /add contact point/i }),
saveContactButton: byRole('button', { name: /save contact point/i }),
newContactPointIntegrationButton: byRole('button', { name: /add contact point integration/i }),
testContactPointButton: byRole('button', { name: /Test/ }),
testContactPointModal: byRole('heading', { name: /test contact point/i }),
customContactPointOption: byRole('radio', { name: /custom/i }),
contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`),
contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`),
contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`),
testContactPoint: byRole('button', { name: /send test notification/i }),
cancelButton: byTestId('cancel-button'),
receiversTable: byTestId('dynamic-table'),
templatesTable: byTestId('templates-table'),
alertManagerPicker: byTestId('alertmanager-picker'),
channelFormContainer: byTestId('item-container'),
contactPointsCollapseToggle: byTestId('collapse-toggle'),
inputs: {
name: byPlaceholderText('Name'),
email: {
addresses: byLabelText(/Addresses/),
toEmails: byLabelText(/To/),
},
hipchat: {
url: byLabelText('Hip Chat Url'),
apiKey: byLabelText('API Key'),
},
slack: {
webhookURL: byLabelText(/Webhook URL/i),
},
webhook: {
URL: byLabelText(/The endpoint to send HTTP POST requests to/i),
},
},
};
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
await userEvent.click(byRole('combobox').get(selectElement));
await selectOptionInTest(selectElement, optionText);
};
document.addEventListener('click', interceptLinkClicks);
const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 };
const useGetGrafanaReceiverTypeCheckerMock = jest.spyOn(grafanaApp, 'useGetGrafanaReceiverTypeChecker');
const server = setupMswServer();
describe('Receivers', () => {
beforeEach(() => {
server.resetHandlers();
jest.resetAllMocks();
mockApi(server).grafanaNotifiers(grafanaNotifiersMock);
mockApi(server).plugins.getPluginSettings(onCallPluginMetaMock);
useGetGrafanaReceiverTypeCheckerMock.mockReturnValue(() => undefined);
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false });
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
setDataSourceSrv(new MockDataSourceSrv(dataSources));
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY);
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
});
it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockImplementation((name) =>
Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig)
);
renderReceivers();
// check that by default grafana templates & receivers are fetched rendered in appropriate tables
await ui.receiversTable.find();
let templatesTable = await ui.templatesTable.find();
let templateRows = templatesTable.querySelectorAll('tbody tr');
expect(templateRows).toHaveLength(3);
expect(templateRows[0]).toHaveTextContent('first template');
expect(templateRows[1]).toHaveTextContent('second template');
expect(templateRows[2]).toHaveTextContent('third template');
let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('default');
expect(receiverRows[1]).toHaveTextContent('critical');
expect(receiverRows).toHaveLength(2);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME);
expect(mocks.api.fetchNotifiers).toHaveBeenCalledTimes(1);
expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual(undefined);
});
it('Grafana receiver can be tested', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
renderReceivers();
// go to new contact point page
await userEvent.click(await ui.newContactPointButton.find());
await byRole('heading', { name: /create contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
// type in a name for the new receiver
await userEvent.type(ui.inputs.name.get(), 'my new receiver');
// enter some email
const email = ui.inputs.email.addresses.get();
await userEvent.clear(email);
await userEvent.type(email, 'tester@grafana.com');
// try to test the contact point
await userEvent.click(await ui.testContactPointButton.find());
await waitFor(() => expect(ui.testContactPointModal.get()).toBeInTheDocument(), { timeout: 1000 });
await userEvent.click(ui.customContactPointOption.get());
// enter custom annotations and labels
await userEvent.type(screen.getByPlaceholderText('Enter a description...'), 'Test contact point');
await userEvent.type(ui.contactPointLabelKey(0).get(), 'foo');
await userEvent.type(ui.contactPointLabelValue(0).get(), 'bar');
await userEvent.click(ui.testContactPoint.get());
await waitFor(() => expect(mocks.api.testReceivers).toHaveBeenCalled());
expect(mocks.api.testReceivers).toHaveBeenCalledWith(
'grafana',
[
{
grafana_managed_receiver_configs: [
{
disableResolveMessage: false,
name: 'test',
secureSettings: {},
settings: { addresses: 'tester@grafana.com', singleEmail: false },
type: 'email',
},
],
name: 'test',
},
],
{ annotations: { description: 'Test contact point' }, labels: { foo: 'bar' } }
);
});
it('Grafana receiver can be created', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
renderReceivers();
// go to new contact point page
await userEvent.click(await ui.newContactPointButton.find());
await byRole('heading', { name: /create contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
// type in a name for the new receiver
await userEvent.type(byPlaceholderText('Name').get(), 'my new receiver');
// check that default email form is rendered
await ui.inputs.email.addresses.find();
// select hipchat
await clickSelectOption(byTestId('items.0.type').get(), 'HipChat');
// check that email options are gone and hipchat options appear
expect(ui.inputs.email.addresses.query()).not.toBeInTheDocument();
const urlInput = ui.inputs.hipchat.url.get();
const apiKeyInput = ui.inputs.hipchat.apiKey.get();
await userEvent.type(urlInput, 'http://hipchat');
await userEvent.type(apiKeyInput, 'foobarbaz');
await userEvent.click(await ui.saveContactButton.find());
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, {
...someGrafanaAlertManagerConfig,
alertmanager_config: {
...someGrafanaAlertManagerConfig.alertmanager_config,
receivers: [
...(someGrafanaAlertManagerConfig.alertmanager_config.receivers ?? []),
{
name: 'my new receiver',
grafana_managed_receiver_configs: [
{
disableResolveMessage: false,
name: 'my new receiver',
secureSettings: {},
settings: {
apiKey: 'foobarbaz',
url: 'http://hipchat',
},
type: 'hipchat',
},
],
},
],
},
});
});
it('Hides create contact point button for users without permission', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
]);
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
renderReceivers();
await ui.receiversTable.find();
expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
});
it('Cloud alertmanager receiver can be edited', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
renderReceivers('CloudManager');
// click edit button for the receiver
await ui.receiversTable.find();
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
await userEvent.click(byTestId('edit').get(receiverRows[0]));
// check that form is open
await byRole('heading', { name: /update contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit');
expect(ui.channelFormContainer.queryAll()).toHaveLength(2);
// delete the email channel
expect(ui.channelFormContainer.queryAll()).toHaveLength(2);
await userEvent.click(byTestId('items.0.delete-button').get());
expect(ui.channelFormContainer.queryAll()).toHaveLength(1);
// modify webhook url
const slackContainer = ui.channelFormContainer.get();
await userEvent.click(byText('Optional Slack settings').get(slackContainer));
await userEvent.type(ui.inputs.slack.webhookURL.get(slackContainer), 'http://newgreaturl');
// add confirm button to action
await userEvent.click(byText(/Actions \(1\)/i).get(slackContainer));
await userEvent.click(await byTestId('items.1.settings.actions.0.confirm.add-button').find());
const confirmSubform = byTestId('items.1.settings.actions.0.confirm.container').get();
await userEvent.type(byLabelText('Text').get(confirmSubform), 'confirm this');
// delete a field
await userEvent.click(byText(/Fields \(2\)/i).get(slackContainer));
await userEvent.click(byTestId('items.1.settings.fields.0.delete-button').get());
byText(/Fields \(1\)/i).get(slackContainer);
// add another channel
await userEvent.click(ui.newContactPointIntegrationButton.get());
await clickSelectOption(await byTestId('items.2.type').find(), 'Webhook');
await userEvent.type(await ui.inputs.webhook.URL.find(), 'http://webhookurl');
await userEvent.click(ui.saveContactButton.get());
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', {
...someCloudAlertManagerConfig,
alertmanager_config: {
...someCloudAlertManagerConfig.alertmanager_config,
receivers: [
{
name: 'cloud-receiver',
slack_configs: [
{
actions: [
{
confirm: {
text: 'confirm this',
},
text: 'action1text',
type: 'action1type',
url: 'http://action1',
},
],
api_url: 'http://slack1http://newgreaturl',
channel: '#mychannel',
fields: [
{
short: false,
title: 'field2',
value: 'text2',
},
],
link_names: false,
send_resolved: false,
short_fields: false,
},
],
webhook_configs: [
{
send_resolved: true,
url: 'http://webhookurl',
},
],
},
],
},
});
});
it('Prometheus Alertmanager receiver cannot be edited', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchStatus.mockResolvedValue({
...someCloudAlertManagerStatus,
config: someCloudAlertManagerConfig.alertmanager_config,
});
renderReceivers(dataSources.promAlertManager.name);
await ui.receiversTable.find();
// there's no templates table for vanilla prom, API does not return templates
expect(ui.templatesTable.query()).not.toBeInTheDocument();
// click view button on the receiver
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('cloud-receiver');
expect(byTestId('edit').query(receiverRows[0])).not.toBeInTheDocument();
await userEvent.click(byTestId('view').get(receiverRows[0]));
// check that form is open
await byRole('heading', { name: /contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/cloud-receiver/edit');
const channelForms = ui.channelFormContainer.queryAll();
expect(channelForms).toHaveLength(2);
// check that inputs are disabled and there is no save button
expect(ui.inputs.name.queryAll()[0]).toHaveAttribute('readonly');
expect(ui.inputs.email.toEmails.get(channelForms[0])).toHaveAttribute('readonly');
expect(ui.inputs.slack.webhookURL.get(channelForms[1])).toHaveAttribute('readonly');
expect(ui.newContactPointButton.query()).not.toBeInTheDocument();
expect(ui.testContactPointButton.query()).not.toBeInTheDocument();
expect(ui.saveContactButton.query()).not.toBeInTheDocument();
expect(ui.cancelButton.query()).toBeInTheDocument();
expect(mocks.api.fetchConfig).not.toHaveBeenCalled();
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
});
it('Loads config from status endpoint if there is no user config', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
// loading an empty config with make it fetch config from status endpoint
mocks.api.fetchConfig.mockResolvedValue({
template_files: {},
alertmanager_config: {},
});
mocks.api.fetchStatus.mockResolvedValue(someCloudAlertManagerStatus);
renderReceivers('CloudManager');
// check that receiver from the default config is represented
await ui.receiversTable.find();
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('default-email');
// check that both config and status endpoints were called
expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchConfig).toHaveBeenLastCalledWith('CloudManager');
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
expect(mocks.api.fetchStatus).toHaveBeenLastCalledWith('CloudManager');
});
it('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true });
mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' });
renderReceivers('CloudManager');
const templatesTable = await ui.templatesTable.find();
const receiversTable = await ui.receiversTable.find();
expect(templatesTable).toBeInTheDocument();
expect(receiversTable).toBeInTheDocument();
});
describe('Contact points health', () => {
it('Should render error notifications when there are some points state ', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
const receiversMock: ContactPointsState = {
receivers: {
default: {
active: true,
notifiers: {
email: [
{
lastNotifyAttemptError:
'establish connection to server: dial tcp: lookup smtp.example.org on 8.8.8.8:53: no such host',
lastNotifyAttempt: '2022-09-19T15:34:40.696Z',
lastNotifyAttemptDuration: '117.2455ms',
name: 'email[0]',
},
],
},
errorCount: 1,
},
critical: {
active: true,
notifiers: {
slack: [
{
lastNotifyAttempt: '2022-09-19T15:34:40.696Z',
lastNotifyAttemptDuration: '117.2455ms',
name: 'slack[0]',
},
],
pagerduty: [
{
lastNotifyAttempt: '2022-09-19T15:34:40.696Z',
lastNotifyAttemptDuration: '117.2455ms',
name: 'pagerduty',
},
],
},
errorCount: 0,
},
},
errorCount: 1,
};
mocks.hooks.useGetContactPointsState.mockReturnValue(receiversMock);
renderReceivers();
//
await ui.receiversTable.find();
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('1 error');
expect(receiverRows[1]).not.toHaveTextContent('error');
expect(receiverRows[1]).toHaveTextContent('OK');
//should show error in contact points when expanding
// expand contact point detail for default 2 emails - 2 errors
await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[0]));
const defaultDetailTable = screen.getAllByTestId('dynamic-table')[1];
expect(byText('Error').getAll(defaultDetailTable)).toHaveLength(1);
// expand contact point detail for slack and pagerduty - 0 errors
await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[1]));
const criticalDetailTable = screen.getAllByTestId('dynamic-table')[2];
expect(byText('Error').query(criticalDetailTable)).toBeNull();
expect(byText('OK').getAll(criticalDetailTable)).toHaveLength(2);
});
it('Should render no attempt message when there are some points state with null lastNotifyAttempt, and "-" in null values', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
const receiversMock: ContactPointsState = {
receivers: {
default: {
active: true,
notifiers: {
email: [
{
lastNotifyAttemptError:
'establish connection to server: dial tcp: lookup smtp.example.org on 8.8.8.8:53: no such host',
lastNotifyAttempt: '2022-09-19T15:34:40.696Z',
lastNotifyAttemptDuration: '117.2455ms',
name: 'email[0]',
},
],
},
errorCount: 1,
},
critical: {
active: true,
notifiers: {
slack: [
{
lastNotifyAttempt: '0001-01-01T00:00:00.000Z',
lastNotifyAttemptDuration: '0s',
name: 'slack[0]',
},
],
pagerduty: [
{
lastNotifyAttempt: '2022-09-19T15:34:40.696Z',
lastNotifyAttemptDuration: '117.2455ms',
name: 'pagerduty',
},
],
},
errorCount: 0,
},
},
errorCount: 1,
};
mocks.hooks.useGetContactPointsState.mockReturnValue(receiversMock);
renderReceivers();
//
await ui.receiversTable.find();
const receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('1 error');
expect(receiverRows[1]).not.toHaveTextContent('error');
expect(receiverRows[1]).toHaveTextContent('No attempts');
//should show error in contact points when expanding
// expand contact point detail for default 2 emails - 2 errors
await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[0]));
const defaultDetailTable = screen.getAllByTestId('dynamic-table')[1];
expect(byText('Error').getAll(defaultDetailTable)).toHaveLength(1);
// expand contact point detail for slack and pagerduty - 0 errors
await userEvent.click(ui.contactPointsCollapseToggle.get(receiverRows[1]));
const criticalDetailTableRows = within(screen.getAllByTestId('dynamic-table')[2]).getAllByTestId('row');
// should render slack item with no attempt
expect(criticalDetailTableRows[0]).toHaveTextContent('No attempt');
expect(criticalDetailTableRows[0]).toHaveTextContent('--');
//should render pagerduty with no attempt
expect(criticalDetailTableRows[1]).toHaveTextContent('OK');
expect(criticalDetailTableRows[1]).toHaveTextContent('117.2455ms');
});
it('Should not render error notifications when fetching contact points state raises 404 error ', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
renderReceivers();
await ui.receiversTable.find();
//contact points are not expandable
expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument();
//should render receivers, only one dynamic table
let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows[0]).toHaveTextContent('default');
expect(receiverRows[1]).toHaveTextContent('critical');
expect(receiverRows).toHaveLength(2);
});
it('Should render "Unused" warning if a contact point is not used in route configuration', async () => {
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mocks.api.updateConfig.mockResolvedValue();
mocks.api.fetchConfig.mockResolvedValue({
...someGrafanaAlertManagerConfig,
alertmanager_config: { ...someGrafanaAlertManagerConfig.alertmanager_config, route: { receiver: 'default' } },
});
mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState);
renderReceivers();
await ui.receiversTable.find();
//contact points are not expandable
expect(ui.contactPointsCollapseToggle.query()).not.toBeInTheDocument();
//should render receivers, only one dynamic table
let receiverRows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(receiverRows).toHaveLength(2);
expect(receiverRows[0]).toHaveTextContent('default');
expect(receiverRows[1]).toHaveTextContent('critical');
expect(receiverRows[1]).toHaveTextContent('Unused');
});
});
});

View File

@@ -1,107 +0,0 @@
import React, { useEffect } from 'react';
import { Route, RouteChildrenProps, Switch } from 'react-router-dom';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView';
import { EditReceiverView } from '../receivers/EditReceiverView';
import { EditTemplateView } from '../receivers/EditTemplateView';
import { GlobalConfigForm } from '../receivers/GlobalConfigForm';
import { NewReceiverView } from '../receivers/NewReceiverView';
import { NewTemplateView } from '../receivers/NewTemplateView';
import { ReceiversAndTemplatesView } from '../receivers/ReceiversAndTemplatesView';
export interface NotificationErrorProps {
errorCount: number;
}
const Receivers = () => {
const { selectedAlertmanager: alertManagerSourceName } = useAlertmanager();
const dispatch = useDispatch();
const { currentData: config, isLoading: loading, error } = useAlertmanagerConfig(alertManagerSourceName);
const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
useEffect(() => {
if (
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
!(receiverTypes.result || receiverTypes.loading || receiverTypes.error)
) {
dispatch(fetchGrafanaNotifiersAction());
}
}, [alertManagerSourceName, dispatch, receiverTypes]);
if (!alertManagerSourceName) {
return null;
}
return (
<>
{error && !loading && (
<Alert severity="error" title="Error loading Alertmanager config">
{error.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={alertManagerSourceName} />
{loading && !config && <LoadingPlaceholder text="loading configuration..." />}
{config && !error && (
<Switch>
<Route exact={true} path="/alerting/notifications">
<ReceiversAndTemplatesView config={config} alertManagerName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/templates/new">
<NewTemplateView config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/templates/:name/duplicate">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<DuplicateTemplateView
alertManagerSourceName={alertManagerSourceName}
config={config}
templateName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/templates/:name/edit">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<EditTemplateView
alertManagerSourceName={alertManagerSourceName}
config={config}
templateName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/receivers/new">
<NewReceiverView config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
<Route exact={true} path="/alerting/notifications/receivers/:name/edit">
{({ match }: RouteChildrenProps<{ name: string }>) =>
match?.params.name && (
<EditReceiverView
alertManagerSourceName={alertManagerSourceName}
config={config}
receiverName={decodeURIComponent(match?.params.name)}
/>
)
}
</Route>
<Route exact={true} path="/alerting/notifications/global-config">
<GlobalConfigForm config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
</Switch>
)}
</>
);
};
export default Receivers;

View File

@@ -0,0 +1,116 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector';
import { AccessControlAction } from 'app/types';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import NewContactPoint from './NewContactPoint';
import setupGrafanaManagedServer, {
setupSaveEndpointMock,
setupTestEndpointMock,
} from './__mocks__/grafanaManagedServer';
import 'core-js/stable/structured-clone';
const server = setupMswServer();
const user = userEvent.setup();
beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]);
setupGrafanaManagedServer(server);
});
it('should be able to test and save a receiver', async () => {
const testMock = setupTestEndpointMock(server);
const saveMock = setupSaveEndpointMock(server);
render(
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName="grafana">
<NewContactPoint />
</AlertmanagerProvider>,
{ wrapper: TestProvider }
);
// wait for loading to be done
// type in a name for the new receiver
await waitFor(() => {
user.type(ui.inputs.name.get(), 'my new receiver');
});
// enter some email
const email = ui.inputs.email.addresses.get();
await user.clear(email);
await user.type(email, 'tester@grafana.com');
// try to test the contact point
await user.click(await ui.testContactPointButton.find());
await waitFor(
() => {
expect(ui.testContactPointModal.get()).toBeInTheDocument();
},
{ timeout: 1000 }
);
await user.click(ui.customContactPointOption.get());
// enter custom annotations and labels
await user.type(screen.getByPlaceholderText('Enter a description...'), 'Test contact point');
await user.type(ui.contactPointLabelKey(0).get(), 'foo');
await user.type(ui.contactPointLabelValue(0).get(), 'bar');
// click test
await user.click(ui.testContactPoint.get());
// we shouldn't be testing implementation details but when the request is successful
// it can't seem to assert on the success toast
await waitFor(() => {
expect(testMock).toHaveBeenCalled();
expect(testMock.mock.lastCall).toMatchSnapshot();
});
await user.click(ui.saveContactButton.get());
await waitFor(() => {
expect(saveMock).toHaveBeenCalled();
expect(saveMock.mock.lastCall).toMatchSnapshot();
});
});
const ui = {
saveContactButton: byRole('button', { name: /save contact point/i }),
newContactPointIntegrationButton: byRole('button', { name: /add contact point integration/i }),
testContactPointButton: byRole('button', { name: /Test/ }),
testContactPointModal: byRole('heading', { name: /test contact point/i }),
customContactPointOption: byRole('radio', { name: /custom/i }),
contactPointAnnotationSelect: (idx: number) => byTestId(`annotation-key-${idx}`),
contactPointAnnotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
contactPointLabelKey: (idx: number) => byTestId(`label-key-${idx}`),
contactPointLabelValue: (idx: number) => byTestId(`label-value-${idx}`),
testContactPoint: byRole('button', { name: /send test notification/i }),
cancelButton: byTestId('cancel-button'),
channelFormContainer: byTestId('item-container'),
inputs: {
name: byPlaceholderText('Name'),
email: {
addresses: byLabelText(/Addresses/),
toEmails: byLabelText(/To/),
},
hipchat: {
url: byLabelText('Hip Chat Url'),
apiKey: byLabelText('API Key'),
},
slack: {
webhookURL: byLabelText(/Webhook URL/i),
},
webhook: {
URL: byLabelText(/The endpoint to send HTTP POST requests to/i),
},
},
};

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { RouteChildrenProps } from 'react-router-dom';
import { Alert } from '@grafana/ui';
@@ -7,7 +6,7 @@ import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { NewReceiverView } from '../receivers/NewReceiverView';
const NewContactPoint = (_props: RouteChildrenProps) => {
const NewContactPoint = () => {
const { selectedAlertmanager } = useAlertmanager();
const { data, isLoading, error } = useAlertmanagerConfig(selectedAlertmanager);

View File

@@ -1,6 +1,14 @@
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email",
"routes": [
{
"receiver": "provisioned-contact-point"
}
]
},
"receivers": [
{
"name": "grafana-default-email",

View File

@@ -21,13 +21,9 @@
"group_wait": "30s",
"matchers": [],
"mute_time_intervals": [],
"receiver": "email",
"receiver": "some webhook",
"repeat_interval": "5h",
"routes": [
{
"receiver": "mixed"
}
]
"routes": []
},
"templates": []
}

View File

@@ -0,0 +1,70 @@
{
"cluster": {
"name": "01HGAVE6N6RG55BZW8N5ZJ12T3",
"peers": [{ "address": "172.19.0.6:9094", "name": "01HGAVE6N6RG55BZW8N5ZJ12T3" }],
"status": "settling"
},
"config": {
"global": {
"resolve_timeout": "5m",
"http_config": {
"tls_config": { "insecure_skip_verify": false },
"follow_redirects": true,
"enable_http2": true,
"proxy_url": null
},
"smtp_hello": "localhost",
"smtp_smarthost": "",
"smtp_require_tls": true,
"pagerduty_url": "https://events.pagerduty.com/v2/enqueue",
"opsgenie_api_url": "https://api.opsgenie.com/",
"wechat_api_url": "https://qyapi.weixin.qq.com/cgi-bin/",
"victorops_api_url": "https://alert.victorops.com/integrations/generic/20131114/alert/",
"telegram_api_url": "https://api.telegram.org",
"webex_api_url": "https://webexapis.com/v1/messages"
},
"route": {
"receiver": "web.hook",
"group_by": ["alertname"],
"group_wait": "30s",
"group_interval": "5m",
"repeat_interval": "1h"
},
"inhibit_rules": [
{
"source_match": { "severity": "critical" },
"target_match": { "severity": "warning" },
"equal": ["alertname", "dev", "instance"]
}
],
"templates": [],
"receivers": [
{
"name": "web.hook",
"webhook_configs": [
{
"send_resolved": true,
"http_config": {
"tls_config": { "insecure_skip_verify": false },
"follow_redirects": true,
"enable_http2": true,
"proxy_url": null
},
"url": "\u003csecret\u003e",
"url_file": "",
"max_alerts": 0
}
]
}
]
},
"uptime": "2023-11-28T11:36:10.663Z",
"versionInfo": {
"branch": "HEAD",
"buildDate": "20230824-11:09:02",
"buildUser": "root@520df6c16a84",
"goVersion": "go1.20.7",
"revision": "d7b4f0c7322e7151d6e3b1e31cbc15361e295d8d",
"version": "0.26.0"
}
}

View File

@@ -1,5 +1,5 @@
import { rest } from 'msw';
import { SetupServer } from 'msw/lib/node';
import { SetupServer } from 'msw/node';
import { AlertmanagerChoice, AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { ReceiversStateDTO } from 'app/types';
@@ -36,3 +36,33 @@ export default (server: SetupServer) => {
return server;
};
export const setupTestEndpointMock = (server: SetupServer) => {
const mock = jest.fn();
server.use(
rest.post('/api/alertmanager/grafana/config/api/v1/receivers/test', async (req, res, ctx) => {
const requestBody = await req.json();
mock(requestBody);
return res.once(ctx.status(200));
})
);
return mock;
};
export const setupSaveEndpointMock = (server: SetupServer) => {
const mock = jest.fn();
server.use(
rest.post('/api/alertmanager/grafana/config/api/v1/alerts', async (req, res, ctx) => {
const requestBody = await req.json();
mock(requestBody);
return res.once(ctx.status(200));
})
);
return mock;
};

View File

@@ -1,24 +0,0 @@
import { rest } from 'msw';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { ReceiversStateDTO } from 'app/types';
import { setupMswServer } from '../../../mockApi';
import alertmanagerMock from './alertmanager.config.mock.json';
import receiversMock from './receivers.mock.json';
const server = setupMswServer();
server.use(
// this endpoint is a grafana built-in alertmanager
rest.get('/api/alertmanager/grafana/config/api/v1/alerts', (_req, res, ctx) =>
res(ctx.json<AlertManagerCortexConfig>(alertmanagerMock))
),
// this endpoint is only available for the built-in alertmanager
rest.get('/api/alertmanager/grafana/config/api/v1/receivers', (_req, res, ctx) =>
res(ctx.json<ReceiversStateDTO[]>(receiversMock))
)
);
export default server;

View File

@@ -0,0 +1,21 @@
import { rest } from 'msw';
import { SetupServer } from 'msw/lib/node';
import { AlertmanagerStatus } from 'app/plugins/datasource/alertmanager/types';
import vanillaAlertManagerConfig from './alertmanager.vanilla.mock.json';
// this one emulates a mimir server setup
export const VANILLA_ALERTMANAGER_DATASOURCE_UID = 'alertmanager';
export default (server: SetupServer) => {
server.use(
rest.get(`/api/alertmanager/${VANILLA_ALERTMANAGER_DATASOURCE_UID}/api/v2/status`, (_req, res, ctx) =>
res(ctx.json<AlertmanagerStatus>(vanillaAlertManagerConfig))
),
// this endpoint will respond if the OnCall plugin is installed
rest.get('/api/plugins/grafana-oncall-app/settings', (_req, res, ctx) => res(ctx.status(404)))
);
return server;
};

View File

@@ -0,0 +1,147 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should be able to test and save a receiver 1`] = `
[
{
"alert": {
"annotations": {
"description": "Test contact point",
},
"labels": {
"foo": "bar",
},
},
"receivers": [
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "test",
"secureSettings": {},
"settings": {
"addresses": "nteews treerc@egirvaefrana.com",
"singleEmail": false,
},
"type": "email",
},
],
"name": "test",
},
],
},
]
`;
exports[`should be able to test and save a receiver 2`] = `
[
{
"alertmanager_config": {
"receivers": [
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "grafana-default-email",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "xeKQrBrnk",
},
],
"name": "grafana-default-email",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "provisioned-contact-point",
"provenance": "api",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "s8SdCVjnk",
},
],
"name": "provisioned-contact-point",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "lotsa-emails",
"secureFields": {},
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
},
],
"name": "lotsa-emails",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts",
},
"type": "slack",
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
},
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts2",
},
"type": "slack",
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
},
],
"name": "Slack with multiple channels",
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "my ",
"secureSettings": {},
"settings": {
"addresses": "nteews treerc@egirvaefrana.com",
"singleEmail": false,
},
"type": "email",
},
],
"name": "my ",
},
],
"route": {
"receiver": "grafana-default-email",
"routes": [
{
"receiver": "provisioned-contact-point",
},
],
},
},
"template_file_provenances": {},
"template_files": {},
},
]
`;

View File

@@ -30,7 +30,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"name": "grafana-default-email",
"numberOfPolicies": 0,
"numberOfPolicies": 1,
},
{
"grafana_managed_receiver_configs": [
@@ -87,7 +87,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
},
],
"name": "provisioned-contact-point",
"numberOfPolicies": 0,
"numberOfPolicies": 1,
},
{
"grafana_managed_receiver_configs": [

View File

@@ -5,7 +5,7 @@ import { useDebounce } from 'react-use';
import { Stack } from '@grafana/experimental';
import { Button, Field, Icon, Input, useStyles2 } from '@grafana/ui';
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
import { useURLSearchParams } from '../../../hooks/useURLSearchParams';
const ContactPointsFilter = () => {
const styles = useStyles2(getStyles);

View File

@@ -2,9 +2,9 @@ import React from 'react';
import { Alert } from '@grafana/ui';
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { GlobalConfigForm } from '../receivers/GlobalConfigForm';
import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../../state/AlertmanagerContext';
import { GlobalConfigForm } from '../../receivers/GlobalConfigForm';
const NewMessageTemplate = () => {
const { selectedAlertmanager } = useAlertmanager();

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { Alert, LinkButton } from '@grafana/ui';
import { AlertmanagerAction } from '../../../hooks/useAbilities';
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
import { makeAMLink } from '../../../utils/misc';
import { Authorize } from '../../Authorize';
interface GlobalConfigAlertProps {
alertManagerName: string;
}
export const GlobalConfigAlert = ({ alertManagerName }: GlobalConfigAlertProps) => {
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
return (
<Authorize actions={[AlertmanagerAction.UpdateExternalConfiguration]}>
<Alert severity="info" title="Global config for contact points">
<p>
For each external Alertmanager you can define global settings, like server addresses, usernames and password,
for all the supported contact points.
</p>
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
{isVanillaAM ? 'View global config' : 'Edit global config'}
</LinkButton>
</Alert>
</Authorize>
);
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Badge } from '@grafana/ui';
export const UnusedContactPointBadge = () => (
<Badge
text="Unused"
aria-label="unused"
color="orange"
icon="exclamation-triangle"
tooltip="This contact point is not used in any notification policy and it will not receive any alerts"
/>
);

View File

@@ -1,73 +0,0 @@
import React from 'react';
import { Alert, LinkButton, Stack } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc';
import { Authorize } from '../Authorize';
import { ReceiversSection } from './ReceiversSection';
import { ReceiversTable } from './ReceiversTable';
import { TemplatesTable } from './TemplatesTable';
interface Props {
config: AlertManagerCortexConfig;
alertManagerName: string;
}
export const ReceiversAndTemplatesView = ({ config, alertManagerName }: Props) => {
const isGrafanaManagedAlertmanager = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
return (
<Stack direction="column" gap={4}>
<ReceiversTable config={config} alertManagerName={alertManagerName} />
{/* Vanilla flavored Alertmanager does not support editing notification templates via the UI */}
{!isVanillaAM && <TemplatesView config={config} alertManagerName={alertManagerName} />}
{/* Grafana manager Alertmanager does not support global config, Mimir and Cortex do */}
{!isGrafanaManagedAlertmanager && <GlobalConfigAlert alertManagerName={alertManagerName} />}
</Stack>
);
};
export const TemplatesView = ({ config, alertManagerName }: Props) => {
const [createNotificationTemplateSupported, createNotificationTemplateAllowed] = useAlertmanagerAbility(
AlertmanagerAction.CreateNotificationTemplate
);
return (
<ReceiversSection
title="Notification templates"
description="Create notification templates to customize your notifications."
addButtonLabel="Add template"
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
showButton={createNotificationTemplateSupported && createNotificationTemplateAllowed}
>
<TemplatesTable config={config} alertManagerName={alertManagerName} />
</ReceiversSection>
);
};
interface GlobalConfigAlertProps {
alertManagerName: string;
}
export const GlobalConfigAlert = ({ alertManagerName }: GlobalConfigAlertProps) => {
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
return (
<Authorize actions={[AlertmanagerAction.UpdateExternalConfiguration]}>
<Alert severity="info" title="Global config for contact points">
<p>
For each external Alertmanager you can define global settings, like server addresses, usernames and password,
for all the supported contact points.
</p>
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
{isVanillaAM ? 'View global config' : 'Edit global config'}
</LinkButton>
</Alert>
</Authorize>
);
};

View File

@@ -1,242 +0,0 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
import { TestProvider } from 'test/helpers/TestProvider';
import { byRole, byTestId } from 'testing-library-selector';
import { setBackendSrv } from '@grafana/runtime';
import {
AlertManagerCortexConfig,
GrafanaManagedReceiverConfig,
Receiver,
} from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } from 'app/types';
import { backendSrv } from '../../../../../core/services/backend_srv';
import * as receiversApi from '../../api/receiversApi';
import { mockProvisioningApi, setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { ReceiversTable } from './ReceiversTable';
import * as receiversMeta from './grafanaAppReceivers/useReceiversMetadata';
import { ReceiverPluginMetadata } from './grafanaAppReceivers/useReceiversMetadata';
jest.mock('react-virtualized-auto-sizer', () => {
return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 });
});
jest.mock('@grafana/ui', () => ({
...jest.requireActual('@grafana/ui'),
CodeEditor: ({ value }: { value: string }) => <textarea data-testid="code-editor" value={value} readOnly />,
}));
const renderReceieversTable = async (
receivers: Receiver[],
notifiers: NotifierDTO[],
alertmanagerName = 'alertmanager-1'
) => {
const config: AlertManagerCortexConfig = {
template_files: {},
alertmanager_config: {
receivers,
},
};
const store = configureStore();
await store.dispatch(fetchGrafanaNotifiersAction.fulfilled(notifiers, 'initial'));
return render(
<TestProvider store={store}>
<AlertmanagerProvider accessType={'notification'}>
<ReceiversTable config={config} alertManagerName={alertmanagerName} />
</AlertmanagerProvider>
</TestProvider>
);
};
const mockGrafanaReceiver = (type: string): GrafanaManagedReceiverConfig => ({
type,
disableResolveMessage: false,
secureFields: {},
settings: {},
name: type,
});
const mockNotifier = (type: NotifierType, name: string): NotifierDTO => ({
type,
name,
description: 'its a mock',
heading: 'foo',
options: [],
});
const useReceiversMetadata = jest.spyOn(receiversMeta, 'useReceiversMetadata');
const useGetContactPointsStateMock = jest.spyOn(receiversApi, 'useGetContactPointsState');
setBackendSrv(backendSrv);
const server = setupMswServer();
afterEach(() => {
server.resetHandlers();
});
const ui = {
export: {
dialog: byRole('dialog', { name: 'Drawer title Export' }),
jsonTab: byRole('tab', { name: /JSON/ }),
yamlTab: byRole('tab', { name: /YAML/ }),
editor: byTestId('code-editor'),
copyCodeButton: byRole('button', { name: 'Copy code' }),
downloadButton: byRole('button', { name: 'Download' }),
},
};
describe('ReceiversTable', () => {
beforeEach(() => {
jest.resetAllMocks();
const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 };
useGetContactPointsStateMock.mockReturnValue(emptyContactPointsState);
useReceiversMetadata.mockReturnValue(new Map<Receiver, ReceiverPluginMetadata>());
});
it('render receivers with grafana notifiers', async () => {
const receivers: Receiver[] = [
{
name: 'with receivers',
grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')],
},
{
name: 'without receivers',
grafana_managed_receiver_configs: [],
},
];
const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')];
await renderReceieversTable(receivers, notifiers);
const rows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(rows).toHaveLength(2);
expect(rows[0]).toHaveTextContent('with receivers');
expect(rows[0].querySelector('[data-column="Type"]')).toHaveTextContent('Google Chat, Sensu Go');
expect(rows[1]).toHaveTextContent('without receivers');
expect(rows[1].querySelector('[data-column="Type"]')).toHaveTextContent('');
});
it('render receivers with alertmanager notifers', async () => {
const receivers: Receiver[] = [
{
name: 'with receivers',
email_configs: [
{
to: 'domas.lapinskas@grafana.com',
},
],
slack_configs: [],
webhook_configs: [
{
url: 'http://example.com',
},
],
opsgenie_configs: [
{
foo: 'bar',
},
],
foo_configs: [
{
url: 'bar',
},
],
},
{
name: 'without receivers',
},
];
await renderReceieversTable(receivers, []);
const rows = within(screen.getByTestId('dynamic-table')).getAllByTestId('row');
expect(rows).toHaveLength(2);
expect(rows[0]).toHaveTextContent('with receivers');
expect(rows[0].querySelector('[data-column="Type"]')).toHaveTextContent('Email, Webhook, OpsGenie, Foo');
expect(rows[1]).toHaveTextContent('without receivers');
expect(rows[1].querySelector('[data-column="Type"]')).toHaveTextContent('');
});
describe('RBAC Enabled', () => {
describe('Export button', () => {
const receivers: Receiver[] = [
{
name: 'with receivers',
grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')],
},
{
name: 'no receivers',
},
];
const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')];
it('should be visible when user has permissions to read notifications', async () => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME);
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
expect(buttons).toHaveLength(2);
});
});
});
describe('Exporter functionality', () => {
it('Should allow exporting receiver', async () => {
// Arrange
mockProvisioningApi(server).exportReceiver({
yaml: 'Yaml Export Content',
json: 'Json Export Content',
});
const user = userEvent.setup();
const receivers: Receiver[] = [
{
name: 'with receivers',
grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')],
},
{
name: 'no receivers',
},
];
const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')];
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
// Act
await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME);
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
// click first export button
await user.click(buttons[0]);
const drawer = await ui.export.dialog.find();
// Assert
expect(ui.export.yamlTab.get(drawer)).toHaveAttribute('aria-selected', 'true');
await waitFor(() => {
expect(ui.export.editor.get(drawer)).toHaveTextContent('Yaml Export Content');
});
await user.click(ui.export.jsonTab.get(drawer));
await waitFor(() => {
expect(ui.export.editor.get(drawer)).toHaveTextContent('Json Export Content');
});
expect(ui.export.copyCodeButton.get(drawer)).toBeInTheDocument();
expect(ui.export.downloadButton.get(drawer)).toBeInTheDocument();
});
});
});

View File

@@ -1,522 +0,0 @@
import pluralize from 'pluralize';
import React, { useMemo, useState } from 'react';
import { useToggle } from 'react-use';
import { dateTime, dateTimeFormat } from '@grafana/data';
import { Badge, Button, ConfirmModal, Icon, Modal, useStyles2, Stack } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
import { useGetContactPointsState } from '../../api/receiversApi';
import { Authorize } from '../../components/Authorize';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { deleteReceiverAction } from '../../state/actions';
import { getAlertTableStyles } from '../../styles/table';
import { SupportedPlugin } from '../../types/pluginBridges';
import { isReceiverUsed } from '../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc';
import { extractNotifierTypeCounts } from '../../utils/receivers';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { ProvisioningBadge } from '../Provisioning';
import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter';
import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection';
import { ReceiverMetadataBadge } from './grafanaAppReceivers/ReceiverMetadataBadge';
import { ReceiverPluginMetadata, useReceiversMetadata } from './grafanaAppReceivers/useReceiversMetadata';
import { AlertmanagerConfigHealth, useAlertmanagerConfigHealth } from './useAlertmanagerConfigHealth';
interface UpdateActionProps extends ActionProps {
onClickDeleteReceiver: (receiverName: string) => void;
}
function UpdateActions({ alertManagerName, receiverName, onClickDeleteReceiver }: UpdateActionProps) {
return (
<>
<Authorize actions={[AlertmanagerAction.UpdateContactPoint]}>
<ActionIcon
aria-label="Edit"
data-testid="edit"
to={makeAMLink(
`/alerting/notifications/receivers/${encodeURIComponent(receiverName)}/edit`,
alertManagerName
)}
tooltip="Edit contact point"
icon="pen"
/>
</Authorize>
<Authorize actions={[AlertmanagerAction.DeleteContactPoint]}>
<ActionIcon
onClick={() => onClickDeleteReceiver(receiverName)}
tooltip="Delete contact point"
icon="trash-alt"
/>
</Authorize>
</>
);
}
interface ActionProps {
alertManagerName: string;
receiverName: string;
canReadSecrets?: boolean;
}
function ViewAction({ alertManagerName, receiverName }: ActionProps) {
return (
<Authorize actions={[AlertmanagerAction.UpdateContactPoint]}>
<ActionIcon
data-testid="view"
to={makeAMLink(`/alerting/notifications/receivers/${encodeURIComponent(receiverName)}/edit`, alertManagerName)}
tooltip="View contact point"
icon="file-alt"
/>
</Authorize>
);
}
function ExportAction({ receiverName, canReadSecrets = false }: ActionProps) {
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
return (
<Authorize actions={[AlertmanagerAction.ExportContactPoint]}>
<ActionIcon
data-testid="export"
tooltip={
canReadSecrets ? 'Export contact point with decrypted secrets' : 'Export contact point with redacted secrets'
}
icon="download-alt"
onClick={toggleShowExportDrawer}
/>
{showExportDrawer && (
<GrafanaReceiverExporter
receiverName={receiverName}
decrypt={canReadSecrets}
onClose={toggleShowExportDrawer}
/>
)}
</Authorize>
);
}
interface ReceiverErrorProps {
errorCount: number;
errorDetail?: string;
showErrorCount: boolean;
tooltip?: string;
}
function ReceiverError({ errorCount, errorDetail, showErrorCount, tooltip }: ReceiverErrorProps) {
const text = showErrorCount ? `${errorCount} ${pluralize('error', errorCount)}` : 'Error';
const tooltipToRender = tooltip ?? errorDetail ?? 'Error';
return <Badge color="red" icon="exclamation-circle" text={text} tooltip={tooltipToRender} />;
}
interface NotifierHealthProps {
errorsByNotifier: number;
errorDetail?: string;
lastNotify: string;
}
function NotifierHealth({ errorsByNotifier, errorDetail, lastNotify }: NotifierHealthProps) {
const hasErrors = errorsByNotifier > 0;
const noAttempts = isLastNotifyNullDate(lastNotify);
if (hasErrors) {
return <ReceiverError errorCount={errorsByNotifier} errorDetail={errorDetail} showErrorCount={false} />;
}
if (noAttempts) {
return <>No attempts</>;
}
return <Badge color="green" text="OK" />;
}
interface ReceiverHealthProps {
errorsByReceiver: number;
someWithNoAttempt: boolean;
}
function ReceiverHealth({ errorsByReceiver, someWithNoAttempt }: ReceiverHealthProps) {
const hasErrors = errorsByReceiver > 0;
if (hasErrors) {
return (
<ReceiverError
errorCount={errorsByReceiver}
showErrorCount={true}
tooltip="Expand the contact point to see error details."
/>
);
}
if (someWithNoAttempt) {
return <>No attempts</>;
}
return <Badge color="green" text="OK" />;
}
const useContactPointsState = (alertManagerName: string) => {
const contactPointsState = useGetContactPointsState(alertManagerName);
const receivers: ReceiversState = contactPointsState?.receivers ?? {};
const errorStateAvailable = Object.keys(receivers).length > 0;
return { contactPointsState, errorStateAvailable };
};
interface ReceiverItem {
name: string;
types: string[];
provisioned?: boolean;
grafanaAppReceiverType?: SupportedPlugin;
metadata?: ReceiverPluginMetadata;
}
interface NotifierStatus {
lastError?: null | string;
lastNotify: string;
lastNotifyDuration: string;
type: string;
sendResolved?: boolean;
}
type RowTableColumnProps = DynamicTableColumnProps<ReceiverItem>;
type RowItemTableProps = DynamicTableItemProps<ReceiverItem>;
type NotifierTableColumnProps = DynamicTableColumnProps<NotifierStatus>;
type NotifierItemTableProps = DynamicTableItemProps<NotifierStatus>;
interface NotifiersTableProps {
notifiersState: NotifiersState;
}
const isLastNotifyNullDate = (lastNotify: string) => lastNotify === '0001-01-01T00:00:00.000Z';
function LastNotify({ lastNotifyDate }: { lastNotifyDate: string }) {
if (isLastNotifyNullDate(lastNotifyDate)) {
return <>{'-'}</>;
} else {
return (
<Stack alignItems="center">
<div>{`${dateTime(lastNotifyDate).locale('en').fromNow(true)} ago`}</div>
<Icon name="clock-nine" />
<div>{`${dateTimeFormat(lastNotifyDate, { format: 'YYYY-MM-DD HH:mm:ss' })}`}</div>
</Stack>
);
}
}
const possibleNullDurations = ['', '0', '0ms', '0s', '0m', '0h', '0d', '0w', '0y'];
const durationIsNull = (duration: string) => possibleNullDurations.includes(duration);
function NotifiersTable({ notifiersState }: NotifiersTableProps) {
function getNotifierColumns(): NotifierTableColumnProps[] {
return [
{
id: 'health',
label: 'Health',
renderCell: ({ data: { lastError, lastNotify } }) => {
return (
<NotifierHealth
errorsByNotifier={lastError ? 1 : 0}
errorDetail={lastError ?? undefined}
lastNotify={lastNotify}
/>
);
},
size: 0.5,
},
{
id: 'name',
label: 'Name',
renderCell: ({ data: { type }, id }) => <>{`${type}[${id}]`}</>,
size: 1,
},
{
id: 'lastNotify',
label: 'Last delivery attempt',
renderCell: ({ data: { lastNotify } }) => <LastNotify lastNotifyDate={lastNotify} />,
size: 3,
},
{
id: 'lastNotifyDuration',
label: 'Last duration',
renderCell: ({ data: { lastNotify, lastNotifyDuration } }) => (
<>{isLastNotifyNullDate(lastNotify) && durationIsNull(lastNotifyDuration) ? '-' : lastNotifyDuration}</>
),
size: 1,
},
{
id: 'sendResolved',
label: 'Send resolved',
renderCell: ({ data: { sendResolved } }) => <>{String(Boolean(sendResolved))}</>,
size: 1,
},
];
}
const notifierRows: NotifierItemTableProps[] = Object.entries(notifiersState).flatMap((typeState) =>
typeState[1].map((notifierStatus, index) => {
return {
id: index,
data: {
type: typeState[0],
lastError: notifierStatus.lastNotifyAttemptError,
lastNotify: notifierStatus.lastNotifyAttempt,
lastNotifyDuration: notifierStatus.lastNotifyAttemptDuration,
sendResolved: notifierStatus.sendResolved,
},
};
})
);
return <DynamicTable items={notifierRows} cols={getNotifierColumns()} pagination={{ itemsPerPage: 25 }} />;
}
interface Props {
config: AlertManagerCortexConfig;
alertManagerName: string;
}
export const ReceiversTable = ({ config, alertManagerName }: Props) => {
const dispatch = useDispatch();
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
const configHealth = useAlertmanagerConfigHealth(config.alertmanager_config);
const { contactPointsState, errorStateAvailable } = useContactPointsState(alertManagerName);
const receiversMetadata = useReceiversMetadata(config.alertmanager_config.receivers ?? []);
// receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted
const [receiverToDelete, setReceiverToDelete] = useState<string>();
const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false);
const [supportsExport, allowedToExport] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
const showExport = supportsExport && allowedToExport;
const onClickDeleteReceiver = (receiverName: string): void => {
if (isReceiverUsed(receiverName, config)) {
setShowCannotDeleteReceiverModal(true);
} else {
setReceiverToDelete(receiverName);
}
};
const deleteReceiver = () => {
if (receiverToDelete) {
dispatch(deleteReceiverAction(receiverToDelete, alertManagerName));
}
setReceiverToDelete(undefined);
};
const rows: RowItemTableProps[] = useMemo(() => {
const receivers = config.alertmanager_config.receivers ?? [];
return (
receivers.map((receiver) => ({
id: receiver.name,
data: {
name: receiver.name,
types: Object.entries(extractNotifierTypeCounts(receiver, grafanaNotifiers.result ?? [])).map(
([type, count]) => {
if (count > 1) {
return `${type} (${count})`;
}
return type;
}
),
provisioned: receiver.grafana_managed_receiver_configs?.some((receiver) => receiver.provenance),
metadata: receiversMetadata.get(receiver),
},
})) ?? []
);
}, [grafanaNotifiers.result, config.alertmanager_config, receiversMetadata]);
const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
const [_, canReadSecrets] = useAlertmanagerAbility(AlertmanagerAction.DecryptSecrets);
const columns = useGetColumns(
alertManagerName,
errorStateAvailable,
contactPointsState,
configHealth,
onClickDeleteReceiver,
isVanillaAM,
canReadSecrets
);
return (
<ReceiversSection
canReadSecrets={canReadSecrets}
title="Contact points"
description="Define where notifications are sent, for example, email or Slack."
showButton={createSupported && createAllowed}
addButtonLabel={'Add contact point'}
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
showExport={showExport}
>
<DynamicTable
pagination={{ itemsPerPage: 25 }}
items={rows}
cols={columns}
isExpandable={errorStateAvailable}
renderExpandedContent={
errorStateAvailable
? ({ data: { name } }) => (
<NotifiersTable notifiersState={contactPointsState?.receivers[name]?.notifiers ?? {}} />
)
: undefined
}
/>
{!!showCannotDeleteReceiverModal && (
<Modal
isOpen={true}
title="Cannot delete contact point"
onDismiss={() => setShowCannotDeleteReceiverModal(false)}
>
<p>
Contact point cannot be deleted because it is used in more policies. Please update or delete these policies
first.
</p>
<Modal.ButtonRow>
<Button variant="secondary" onClick={() => setShowCannotDeleteReceiverModal(false)} fill="outline">
Close
</Button>
</Modal.ButtonRow>
</Modal>
)}
{!!receiverToDelete && (
<ConfirmModal
isOpen={true}
title="Delete contact point"
body={`Are you sure you want to delete contact point "${receiverToDelete}"?`}
confirmText="Yes, delete"
onConfirm={deleteReceiver}
onDismiss={() => setReceiverToDelete(undefined)}
/>
)}
</ReceiversSection>
);
};
const errorsByReceiver = (contactPointsState: ContactPointsState, receiverName: string) =>
contactPointsState?.receivers[receiverName]?.errorCount ?? 0;
const someNotifiersWithNoAttempt = (contactPointsState: ContactPointsState, receiverName: string) => {
const notifiers = Object.values(contactPointsState?.receivers[receiverName]?.notifiers ?? {});
if (notifiers.length === 0) {
return false;
}
const hasSomeWitNoAttempt = notifiers.flat().some((status) => isLastNotifyNullDate(status.lastNotifyAttempt));
return hasSomeWitNoAttempt;
};
function useGetColumns(
alertManagerName: string,
errorStateAvailable: boolean,
contactPointsState: ContactPointsState | undefined,
configHealth: AlertmanagerConfigHealth,
onClickDeleteReceiver: (receiverName: string) => void,
isVanillaAM: boolean,
canReadSecrets: boolean
): RowTableColumnProps[] {
const tableStyles = useStyles2(getAlertTableStyles);
const enableHealthColumn =
errorStateAvailable || Object.values(configHealth.contactPoints).some((cp) => cp.matchingRoutes === 0);
const isGrafanaAlertManager = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
const baseColumns: RowTableColumnProps[] = [
{
id: 'name',
label: 'Contact point name',
renderCell: ({ data: { name, provisioned } }) => (
<>
<div>{name}</div>
{provisioned && <ProvisioningBadge />}
</>
),
size: 3,
className: tableStyles.nameCell,
},
{
id: 'type',
label: 'Type',
renderCell: ({ data: { types, metadata } }) => (
<>{metadata ? <ReceiverMetadataBadge metadata={metadata} /> : types.join(', ')}</>
),
size: 2,
},
];
const healthColumn: RowTableColumnProps = {
id: 'health',
label: 'Health',
renderCell: ({ data: { name } }) => {
if (configHealth.contactPoints[name]?.matchingRoutes === 0) {
return <UnusedContactPointBadge />;
}
return (
contactPointsState &&
Object.entries(contactPointsState.receivers).length > 0 && (
<ReceiverHealth
errorsByReceiver={errorsByReceiver(contactPointsState, name)}
someWithNoAttempt={someNotifiersWithNoAttempt(contactPointsState, name)}
/>
)
);
},
size: '160px',
};
return [
...baseColumns,
...(enableHealthColumn ? [healthColumn] : []),
{
id: 'actions',
label: 'Actions',
renderCell: ({ data: { provisioned, name } }) => (
<Authorize
actions={[
AlertmanagerAction.UpdateContactPoint,
AlertmanagerAction.DeleteContactPoint,
AlertmanagerAction.ExportContactPoint,
]}
>
<div className={tableStyles.actionsCell}>
{!isVanillaAM && !provisioned && (
<UpdateActions
alertManagerName={alertManagerName}
receiverName={name}
onClickDeleteReceiver={onClickDeleteReceiver}
/>
)}
{(isVanillaAM || provisioned) && <ViewAction alertManagerName={alertManagerName} receiverName={name} />}
{isGrafanaAlertManager && (
<ExportAction alertManagerName={alertManagerName} receiverName={name} canReadSecrets={canReadSecrets} />
)}
</div>
</Authorize>
),
size: '100px',
},
];
}
export function UnusedContactPointBadge() {
return (
<Badge
text="Unused"
color="orange"
icon="exclamation-triangle"
tooltip="This contact point is not used in any notification policy and it will not receive any alerts"
/>
);
}

View File

@@ -5,7 +5,7 @@ import { usePluginBridge } from '../../../hooks/usePluginBridge';
import { SupportedPlugin } from '../../../types/pluginBridges';
import { isOnCallReceiver } from './onCall/onCall';
import { AmRouteReceiver, ReceiverWithTypes } from './types';
import { AmRouteReceiver } from './types';
export const useGetGrafanaReceiverTypeChecker = () => {
const { installed: isOnCallEnabled } = usePluginBridge(SupportedPlugin.OnCall);
@@ -38,13 +38,3 @@ export const useGetAmRouteReceiverWithGrafanaAppTypes = (receivers: Receiver[])
return receivers.map(receiverToSelectableContactPointValue);
};
export const useGetReceiversWithGrafanaAppTypes = (receivers: Receiver[]): ReceiverWithTypes[] => {
const getGrafanaReceiverType = useGetGrafanaReceiverTypeChecker();
return receivers.map((receiver: Receiver) => {
return {
...receiver,
grafanaAppReceiverType: getGrafanaReceiverType(receiver),
};
});
};

View File

@@ -1,4 +1,3 @@
import { GrafanaManagedContactPoint } from '../../../../../../plugins/datasource/alertmanager/types';
import { SupportedPlugin } from '../../../types/pluginBridges';
export interface AmRouteReceiver {
@@ -7,9 +6,6 @@ export interface AmRouteReceiver {
grafanaAppReceiverType?: SupportedPlugin;
}
export interface ReceiverWithTypes extends GrafanaManagedContactPoint {
grafanaAppReceiverType?: SupportedPlugin;
}
export const GRAFANA_APP_RECEIVERS_SOURCE_IMAGE: Record<SupportedPlugin, string> = {
[SupportedPlugin.OnCall]: 'public/img/alerting/oncall_logo.svg',

View File

@@ -1,12 +1,8 @@
import { useMemo } from 'react';
import { GrafanaManagedReceiverConfig, Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
import { onCallApi, OnCallIntegrationDTO } from '../../../api/onCallApi';
import { usePluginBridge } from '../../../hooks/usePluginBridge';
import { GrafanaManagedReceiverConfig } from '../../../../../../plugins/datasource/alertmanager/types';
import { OnCallIntegrationDTO } from '../../../api/onCallApi';
import { SupportedPlugin } from '../../../types/pluginBridges';
import { createBridgeURL } from '../../PluginBridge';
import { ReceiverTypes } from './onCall/onCall';
import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types';
export interface ReceiverPluginMetadata {
@@ -25,32 +21,6 @@ const onCallReceiverMeta: ReceiverPluginMetadata = {
icon: onCallReceiverICon,
};
export const useReceiversMetadata = (receivers: Receiver[]): Map<Receiver, ReceiverPluginMetadata> => {
const { installed: isOnCallEnabled } = usePluginBridge(SupportedPlugin.OnCall);
const { data: onCallIntegrations = [] } = onCallApi.useGrafanaOnCallIntegrationsQuery(undefined, {
skip: !isOnCallEnabled,
});
return useMemo(() => {
const result = new Map<Receiver, ReceiverPluginMetadata>();
receivers.forEach((receiver) => {
const onCallReceiver = receiver.grafana_managed_receiver_configs?.find((c) => c.type === ReceiverTypes.OnCall);
if (onCallReceiver) {
if (!isOnCallEnabled) {
result.set(receiver, getOnCallMetadata(null, onCallReceiver));
return;
}
result.set(receiver, getOnCallMetadata(onCallIntegrations, onCallReceiver));
}
});
return result;
}, [receivers, isOnCallEnabled, onCallIntegrations]);
};
export function getOnCallMetadata(
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
receiver: GrafanaManagedReceiverConfig

View File

@@ -1,35 +0,0 @@
import { countBy } from 'lodash';
import { AlertmanagerConfig } from '../../../../../plugins/datasource/alertmanager/types';
import { getUsedContactPoints } from '../contact-points/utils';
export interface ContactPointConfigHealth {
matchingRoutes: number;
}
export interface AlertmanagerConfigHealth {
contactPoints: Record<string, ContactPointConfigHealth>;
}
export function useAlertmanagerConfigHealth(config: AlertmanagerConfig): AlertmanagerConfigHealth {
if (!config.receivers) {
return { contactPoints: {} };
}
if (!config.route) {
return { contactPoints: Object.fromEntries(config.receivers.map((r) => [r.name, { matchingRoutes: 0 }])) };
}
const definedContactPointNames = config.receivers?.map((receiver) => receiver.name) ?? [];
const usedContactPoints = getUsedContactPoints(config.route);
const usedContactPointCounts = countBy(usedContactPoints);
const contactPointsHealth: AlertmanagerConfigHealth['contactPoints'] = {};
const configHealth: AlertmanagerConfigHealth = { contactPoints: contactPointsHealth };
definedContactPointNames.forEach((contactPointName) => {
contactPointsHealth[contactPointName] = { matchingRoutes: usedContactPointCounts[contactPointName] ?? 0 };
});
return configHealth;
}

View File

@@ -6,7 +6,7 @@ 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 { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints';
import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints';
import { selectContactPoint } from './SimplifiedRouting';

View File

@@ -53,18 +53,6 @@ export function renameMuteTimings(newMuteTimingName: string, oldMuteTimingName:
};
}
function isReceiverUsedInRoute(receiver: string, route: Route): boolean {
return (
(route.receiver === receiver || route.routes?.some((route) => isReceiverUsedInRoute(receiver, route))) ?? false
);
}
export function isReceiverUsed(receiver: string, config: AlertManagerCortexConfig): boolean {
return (
(config.alertmanager_config.route && isReceiverUsedInRoute(receiver, config.alertmanager_config.route)) ?? false
);
}
export function matcherToOperator(matcher: Matcher): MatcherOperator {
if (matcher.isEqual) {
if (matcher.isRegex) {

View File

@@ -1,33 +1,6 @@
import { capitalize, isEmpty, times } from 'lodash';
import { isEmpty, times } from 'lodash';
import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts';
import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/alertmanager/types';
import { NotifierDTO } from 'app/types';
// extract notifier type name to count map, eg { Slack: 1, Email: 2 }
type NotifierTypeCounts = Record<string, number>; // name : count
export function extractNotifierTypeCounts(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): NotifierTypeCounts {
if ('grafana_managed_receiver_configs' in receiver) {
return getGrafanaNotifierTypeCounts(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers);
}
return getCortexAlertManagerNotifierTypeCounts(receiver);
}
function getCortexAlertManagerNotifierTypeCounts(receiver: Receiver): NotifierTypeCounts {
return Object.entries(receiver)
.filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) // filter out only properties that are alertmanager notifier
.filter(([_, value]) => Array.isArray(value) && !!value.length) // check that there are actually notifiers of this type configured
.reduce<NotifierTypeCounts>((acc, [key, value]) => {
const type = key.replace('_configs', ''); // remove the `_config` part from the key, making it intto a notifier name
const name = receiverTypeNames[type] ?? capitalize(type);
return {
...acc,
[name]: (acc[name] ?? 0) + (Array.isArray(value) ? value.length : 1),
};
}, {});
}
/**
* This function will extract the integrations that have been defined for either grafana managed contact point
@@ -67,19 +40,3 @@ export function extractReceivers(receiver: Receiver): GrafanaManagedReceiverConf
return integrations;
}
function getGrafanaNotifierTypeCounts(
configs: GrafanaManagedReceiverConfig[],
grafanaNotifiers: NotifierDTO[]
): NotifierTypeCounts {
return configs
.map((recv) => recv.type) // extract types from config
.map((type) => grafanaNotifiers.find((r) => r.type === type)?.name ?? capitalize(type)) // get readable name from notifier cofnig, or if not available, just capitalize
.reduce<NotifierTypeCounts>(
(acc, type) => ({
...acc,
[type]: (acc[type] ?? 0) + 1,
}),
{}
);
}

View File

@@ -12,30 +12,30 @@ export type AlertManagerCortexConfig = {
};
export type TLSConfig = {
ca_file: string;
cert_file: string;
key_file: string;
ca_file?: string;
cert_file?: string;
key_file?: string;
server_name?: string;
insecure_skip_verify?: boolean;
};
export type HTTPConfigCommon = {
proxy_url?: string;
proxy_url?: string | null;
tls_config?: TLSConfig;
};
export type HTTPConfigBasicAuth = {
basic_auth: {
basic_auth?: {
username: string;
} & ({ password: string } | { password_file: string });
} & ({ password?: string } | { password_file?: string });
};
export type HTTPConfigBearerToken = {
bearer_token: string;
bearer_token?: string;
};
export type HTTPConfigBearerTokenFile = {
bearer_token_file: string;
bearer_token_file?: string;
};
export type HTTPConfig = HTTPConfigCommon & (HTTPConfigBasicAuth | HTTPConfigBearerToken | HTTPConfigBearerTokenFile);
@@ -123,10 +123,10 @@ export interface RouteWithID extends Route {
}
export type InhibitRule = {
target_match: Record<string, string>;
target_match_re: Record<string, string>;
source_match: Record<string, string>;
source_match_re: Record<string, string>;
target_match?: Record<string, string>;
target_match_re?: Record<string, string>;
source_match?: Record<string, string>;
source_match_re?: Record<string, string>;
equal?: string[];
};