mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Remove old contact points view (#78704)
This commit is contained in:
@@ -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": [
|
||||
|
@@ -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 |
|
||||
|
||||
|
@@ -124,7 +124,6 @@ export interface FeatureToggles {
|
||||
lokiRunQueriesInParallel?: boolean;
|
||||
wargamesTesting?: boolean;
|
||||
alertingInsights?: boolean;
|
||||
alertingContactPointsV2?: boolean;
|
||||
externalCorePlugins?: boolean;
|
||||
pluginsAPIMetrics?: boolean;
|
||||
httpSLOLevels?: boolean;
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
|
@@ -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"
|
||||
|
@@ -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>
|
||||
);
|
||||
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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}
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
@@ -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),
|
||||
},
|
||||
},
|
||||
};
|
@@ -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);
|
||||
|
||||
|
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"template_files": {},
|
||||
"alertmanager_config": {
|
||||
"route": {
|
||||
"receiver": "grafana-default-email",
|
||||
"routes": [
|
||||
{
|
||||
"receiver": "provisioned-contact-point"
|
||||
}
|
||||
]
|
||||
},
|
||||
"receivers": [
|
||||
{
|
||||
"name": "grafana-default-email",
|
||||
|
@@ -21,13 +21,9 @@
|
||||
"group_wait": "30s",
|
||||
"matchers": [],
|
||||
"mute_time_intervals": [],
|
||||
"receiver": "email",
|
||||
"receiver": "some webhook",
|
||||
"repeat_interval": "5h",
|
||||
"routes": [
|
||||
{
|
||||
"receiver": "mixed"
|
||||
}
|
||||
]
|
||||
"routes": []
|
||||
},
|
||||
"templates": []
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -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;
|
||||
};
|
||||
|
@@ -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;
|
@@ -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;
|
||||
};
|
@@ -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": {},
|
||||
},
|
||||
]
|
||||
`;
|
@@ -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": [
|
||||
|
@@ -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);
|
@@ -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();
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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"
|
||||
/>
|
||||
);
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
@@ -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),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@@ -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',
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
@@ -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';
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
@@ -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[];
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user