mirror of
https://github.com/grafana/grafana.git
synced 2025-01-15 19:22:34 -06:00
Alerting: Contact Points v2 part IV (#76063)
This commit is contained in:
parent
283f279a17
commit
e12e40fc24
@ -2228,10 +2228,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/ReceiverMetadataBadge.tsx:5381": [
|
||||
[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/rule-editor/AnnotationKeyInput.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
|
@ -80,6 +80,7 @@ Some features are enabled by default. You can disable these feature by setting t
|
||||
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the `useCachingService` feature toggle is enabled and the datasource has caching and async query support enabled |
|
||||
| `splitScopes` | Support faster dashboard and folder search by splitting permission scopes into parts |
|
||||
| `reportingRetries` | Enables rendering retries for the reporting feature |
|
||||
| `alertingContactPointsV2` | Show the new contacpoints list view |
|
||||
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
|
||||
|
||||
## Experimental feature toggles
|
||||
|
@ -128,6 +128,7 @@ export interface FeatureToggles {
|
||||
lokiRunQueriesInParallel?: boolean;
|
||||
wargamesTesting?: boolean;
|
||||
alertingInsights?: boolean;
|
||||
alertingContactPointsV2?: boolean;
|
||||
externalCorePlugins?: boolean;
|
||||
pluginsAPIMetrics?: boolean;
|
||||
httpSLOLevels?: boolean;
|
||||
|
@ -773,6 +773,13 @@ var (
|
||||
Owner: grafanaAlertingSquad,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
{
|
||||
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",
|
||||
|
@ -109,6 +109,7 @@ 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
|
||||
|
|
@ -447,6 +447,10 @@ 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"
|
||||
|
@ -187,6 +187,18 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/notifications/receivers/:id/edit',
|
||||
roles: evaluateAccess([
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
]),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/notifications/:type/:id/edit',
|
||||
roles: evaluateAccess([
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Disable, Enable } from 'react-enable';
|
||||
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'));
|
||||
@ -17,13 +17,14 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynami
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { AlertingFeature } from './features';
|
||||
|
||||
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 => (
|
||||
<AlertmanagerPageWrapper pageId="receivers" accessType="notification">
|
||||
<Enable feature={AlertingFeature.ContactPointsV2}>
|
||||
{/* TODO do we want a "routes" component for each Alerting entity? */}
|
||||
{/* 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} />
|
||||
@ -37,10 +38,9 @@ const ContactPoints = (props: GrafanaRouteComponentProps): JSX.Element => (
|
||||
/>
|
||||
<Route exact={true} path="/alerting/notifications/global-config" component={GlobalConfig} />
|
||||
</Switch>
|
||||
</Enable>
|
||||
<Disable feature={AlertingFeature.ContactPointsV2}>
|
||||
) : (
|
||||
<ContactPointsV1 {...props} />
|
||||
</Disable>
|
||||
)}
|
||||
</AlertmanagerPageWrapper>
|
||||
);
|
||||
|
||||
|
@ -14,12 +14,12 @@ interface GrafanaAlertmanagerDeliveryWarningProps {
|
||||
|
||||
export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi;
|
||||
const { currentData: amChoiceStatus } = useGetAlertmanagerChoiceStatusQuery();
|
||||
|
||||
const viewingInternalAM = currentAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
const { currentData: amChoiceStatus } = alertmanagerApi.endpoints.getAlertmanagerChoiceStatus.useQuery(undefined, {
|
||||
skip: !viewingInternalAM,
|
||||
});
|
||||
|
||||
const interactsWithExternalAMs =
|
||||
amChoiceStatus?.alertmanagersChoice &&
|
||||
[AlertmanagerChoice.External, AlertmanagerChoice.All].includes(amChoiceStatus?.alertmanagersChoice);
|
||||
|
@ -0,0 +1,24 @@
|
||||
import React, { forwardRef, Ref } from 'react';
|
||||
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, ButtonProps, Icon } from '@grafana/ui';
|
||||
|
||||
const MoreButton = forwardRef(function MoreButton(props: ButtonProps, ref: Ref<HTMLButtonElement>) {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={0}>
|
||||
More <Icon name="angle-down" />
|
||||
</Stack>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
export default MoreButton;
|
@ -58,6 +58,72 @@ describe('ContactPoints', () => {
|
||||
expect(screen.getByText('grafana-default-email')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('contact-point')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should call delete when clicked and not disabled', async () => {
|
||||
const onDelete = jest.fn();
|
||||
|
||||
render(<ContactPoint name={'my-contact-point'} receivers={[]} onDelete={onDelete} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
await userEvent.click(moreActions);
|
||||
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
expect(onDelete).toHaveBeenCalledWith('my-contact-point');
|
||||
});
|
||||
|
||||
it('should disable edit button', async () => {
|
||||
render(<ContactPoint name={'my-contact-point'} disabled={true} receivers={[]} onDelete={noop} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
expect(moreActions).not.toBeDisabled();
|
||||
|
||||
const editAction = screen.getByTestId('edit-action');
|
||||
expect(editAction).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('should disable buttons when provisioned', async () => {
|
||||
render(<ContactPoint name={'my-contact-point'} provisioned={true} receivers={[]} onDelete={noop} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(screen.getByText(/provisioned/i)).toBeInTheDocument();
|
||||
|
||||
const editAction = screen.queryByTestId('edit-action');
|
||||
expect(editAction).not.toBeInTheDocument();
|
||||
|
||||
const viewAction = screen.getByRole('link', { name: /view/i });
|
||||
expect(viewAction).toBeInTheDocument();
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
expect(moreActions).not.toBeDisabled();
|
||||
await userEvent.click(moreActions);
|
||||
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable delete when contact point is linked to at least one notification policy', async () => {
|
||||
render(
|
||||
<ContactPoint name={'my-contact-point'} provisioned={true} receivers={[]} policies={1} onDelete={noop} />,
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument();
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
await userEvent.click(moreActions);
|
||||
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mimir-flavored alertmanager', () => {
|
||||
@ -98,71 +164,6 @@ describe('ContactPoints', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ContactPoint', () => {
|
||||
it('should call delete when clicked and not disabled', async () => {
|
||||
const onDelete = jest.fn();
|
||||
|
||||
render(<ContactPoint name={'my-contact-point'} receivers={[]} onDelete={onDelete} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
await userEvent.click(moreActions);
|
||||
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
expect(onDelete).toHaveBeenCalledWith('my-contact-point');
|
||||
});
|
||||
|
||||
it('should disable edit button', async () => {
|
||||
render(<ContactPoint name={'my-contact-point'} disabled={true} receivers={[]} onDelete={noop} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
expect(moreActions).not.toBeDisabled();
|
||||
|
||||
const editAction = screen.getByTestId('edit-action');
|
||||
expect(editAction).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('should disable buttons when provisioned', async () => {
|
||||
render(<ContactPoint name={'my-contact-point'} provisioned={true} receivers={[]} onDelete={noop} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(screen.getByText(/provisioned/i)).toBeInTheDocument();
|
||||
|
||||
const editAction = screen.queryByTestId('edit-action');
|
||||
expect(editAction).not.toBeInTheDocument();
|
||||
|
||||
const viewAction = screen.getByRole('link', { name: /view/i });
|
||||
expect(viewAction).toBeInTheDocument();
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
expect(moreActions).not.toBeDisabled();
|
||||
await userEvent.click(moreActions);
|
||||
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable delete when contact point is linked to at least one notification policy', async () => {
|
||||
render(<ContactPoint name={'my-contact-point'} provisioned={true} receivers={[]} policies={1} onDelete={noop} />, {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'is used by 1 notification policy' })).toBeInTheDocument();
|
||||
|
||||
const moreActions = screen.getByRole('button', { name: 'more-actions' });
|
||||
await userEvent.click(moreActions);
|
||||
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: PropsWithChildren) => (
|
||||
<TestProvider>
|
||||
<AlertmanagerProvider accessType={'notification'}>{children}</AlertmanagerProvider>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { SerializedError } from '@reduxjs/toolkit';
|
||||
import { groupBy, size, uniqueId, upperFirst } from 'lodash';
|
||||
import { groupBy, size, upperFirst } from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import React, { Fragment, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { dateTime, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
Icon,
|
||||
LoadingPlaceholder,
|
||||
@ -22,47 +22,65 @@ import {
|
||||
TabContent,
|
||||
Tab,
|
||||
Pagination,
|
||||
Button,
|
||||
} from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
||||
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
|
||||
import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts';
|
||||
import { GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { usePagination } from '../../hooks/usePagination';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
|
||||
import { MetaText } from '../MetaText';
|
||||
import MoreButton from '../MoreButton';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
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 { MessageTemplates } from './MessageTemplates';
|
||||
import { useDeleteContactPointModal } from './Modals';
|
||||
import { RECEIVER_STATUS_KEY, useContactPointsWithStatus, useDeleteContactPoint } from './useContactPoints';
|
||||
import { ContactPointWithStatus, getReceiverDescription, isProvisioned, ReceiverConfigWithStatus } from './utils';
|
||||
import {
|
||||
RECEIVER_META_KEY,
|
||||
RECEIVER_PLUGIN_META_KEY,
|
||||
RECEIVER_STATUS_KEY,
|
||||
useContactPointsWithStatus,
|
||||
useDeleteContactPoint,
|
||||
} from './useContactPoints';
|
||||
import { ContactPointWithMetadata, getReceiverDescription, isProvisioned, ReceiverConfigWithMetadata } from './utils';
|
||||
|
||||
enum ActiveTab {
|
||||
ContactPoints,
|
||||
MessageTemplates,
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
const ContactPoints = () => {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
// TODO hook up to query params
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>(ActiveTab.ContactPoints);
|
||||
let { isLoading, error, contactPoints } = useContactPointsWithStatus(selectedAlertmanager!);
|
||||
let { isLoading, error, contactPoints } = useContactPointsWithStatus();
|
||||
const { deleteTrigger, updateAlertmanagerState } = useDeleteContactPoint(selectedAlertmanager!);
|
||||
const [addContactPointSupported, addContactPointAllowed] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.CreateContactPoint
|
||||
);
|
||||
const [exportContactPointsSupported, exportContactPointsAllowed] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.ExportContactPoint
|
||||
);
|
||||
|
||||
const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger, updateAlertmanagerState.isLoading);
|
||||
const [ExportDrawer, showExportDrawer] = useExportContactPoint();
|
||||
|
||||
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
|
||||
const showingMessageTemplates = activeTab === ActiveTab.MessageTemplates;
|
||||
@ -73,13 +91,11 @@ const ContactPoints = () => {
|
||||
}
|
||||
|
||||
const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
const isVanillaAlertmanager = isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager!);
|
||||
const permissions = getNotificationsPermissions(selectedAlertmanager!);
|
||||
|
||||
const allowedToAddContactPoint = contextSrv.hasPermission(permissions.create);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
|
||||
<Stack direction="column">
|
||||
<TabsBar>
|
||||
<Tab
|
||||
@ -93,23 +109,6 @@ const ContactPoints = () => {
|
||||
active={showingMessageTemplates}
|
||||
onChangeTab={() => setActiveTab(ActiveTab.MessageTemplates)}
|
||||
/>
|
||||
<Spacer />
|
||||
{showingContactPoints && (
|
||||
<LinkButton
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
href="/alerting/notifications/receivers/new"
|
||||
// TODO clarify why the button has been disabled
|
||||
disabled={!allowedToAddContactPoint || isVanillaAlertmanager}
|
||||
>
|
||||
Add contact point
|
||||
</LinkButton>
|
||||
)}
|
||||
{showingMessageTemplates && (
|
||||
<LinkButton icon="plus" variant="primary" href="/alerting/notifications/templates/new">
|
||||
Add message template
|
||||
</LinkButton>
|
||||
)}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
<Stack direction="column">
|
||||
@ -123,9 +122,34 @@ const ContactPoints = () => {
|
||||
) : (
|
||||
<>
|
||||
{/* TODO we can add some additional info here with a ToggleTip */}
|
||||
<Text variant="body" color="secondary">
|
||||
Define where notifications are sent, a contact point can contain multiple integrations.
|
||||
</Text>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Text variant="body" color="secondary">
|
||||
Define where notifications are sent, a contact point can contain multiple integrations.
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Stack direction="row" gap={1}>
|
||||
{addContactPointSupported && (
|
||||
<LinkButton
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
href="/alerting/notifications/receivers/new"
|
||||
disabled={!addContactPointAllowed}
|
||||
>
|
||||
Add contact point
|
||||
</LinkButton>
|
||||
)}
|
||||
{exportContactPointsSupported && (
|
||||
<Button
|
||||
icon="download-alt"
|
||||
variant="secondary"
|
||||
disabled={!exportContactPointsAllowed}
|
||||
onClick={() => showExportDrawer(ALL_CONTACT_POINTS)}
|
||||
>
|
||||
Export all
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ContactPointsList
|
||||
contactPoints={contactPoints}
|
||||
pageSize={DEFAULT_PAGE_SIZE}
|
||||
@ -141,9 +165,15 @@ const ContactPoints = () => {
|
||||
{/* Message Templates tab */}
|
||||
{showingMessageTemplates && (
|
||||
<>
|
||||
<Text variant="body" color="secondary">
|
||||
Create message templates to customize your notifications.
|
||||
</Text>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Text variant="body" color="secondary">
|
||||
Create message templates to customize your notifications.
|
||||
</Text>
|
||||
<Spacer />
|
||||
<LinkButton icon="plus" variant="primary" href="/alerting/notifications/templates/new">
|
||||
Add message template
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
<MessageTemplates />
|
||||
</>
|
||||
)}
|
||||
@ -152,12 +182,13 @@ const ContactPoints = () => {
|
||||
</TabContent>
|
||||
</Stack>
|
||||
{DeleteModal}
|
||||
{ExportDrawer}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContactPointsListProps {
|
||||
contactPoints: ContactPointWithStatus[];
|
||||
contactPoints: ContactPointWithMetadata[];
|
||||
disabled?: boolean;
|
||||
onDelete: (name: string) => void;
|
||||
pageSize?: number;
|
||||
@ -198,7 +229,7 @@ interface ContactPointProps {
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
provisioned?: boolean;
|
||||
receivers: ReceiverConfigWithStatus[];
|
||||
receivers: ReceiverConfigWithMetadata[];
|
||||
policies?: number;
|
||||
onDelete: (name: string) => void;
|
||||
}
|
||||
@ -228,16 +259,21 @@ export const ContactPoint = ({
|
||||
/>
|
||||
{showFullMetadata ? (
|
||||
<div>
|
||||
{receivers?.map((receiver) => {
|
||||
{receivers.map((receiver, index) => {
|
||||
const diagnostics = receiver[RECEIVER_STATUS_KEY];
|
||||
const metadata = receiver[RECEIVER_META_KEY];
|
||||
const sendingResolved = !Boolean(receiver.disableResolveMessage);
|
||||
const pluginMetadata = receiver[RECEIVER_PLUGIN_META_KEY];
|
||||
const key = metadata.name + index;
|
||||
|
||||
return (
|
||||
<ContactPointReceiver
|
||||
key={uniqueId()}
|
||||
key={key}
|
||||
name={metadata.name}
|
||||
type={receiver.type}
|
||||
description={getReceiverDescription(receiver)}
|
||||
diagnostics={diagnostics}
|
||||
pluginMetadata={pluginMetadata}
|
||||
sendingResolved={sendingResolved}
|
||||
/>
|
||||
);
|
||||
@ -264,15 +300,55 @@ interface ContactPointHeaderProps {
|
||||
const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
||||
const { name, disabled = false, provisioned = false, policies = 0, onDelete } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const permissions = getNotificationsPermissions(selectedAlertmanager ?? '');
|
||||
|
||||
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
|
||||
const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
const [deleteSupported, deleteAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
|
||||
const [ExportDrawer, openExportDrawer] = useExportContactPoint();
|
||||
|
||||
const isReferencedByPolicies = policies > 0;
|
||||
const isGranaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
const canEdit = editSupported && editAllowed && !provisioned;
|
||||
const canDelete = deleteSupported && deleteAllowed && !provisioned && policies === 0;
|
||||
|
||||
// we make a distinction here becase for "canExport" we show the menu item, if not we hide it
|
||||
const canExport = isGranaManagedAlertmanager;
|
||||
const allowedToExport = contextSrv.hasPermission(permissions.provisioning.read);
|
||||
const menuActions: JSX.Element[] = [];
|
||||
|
||||
if (exportSupported) {
|
||||
menuActions.push(
|
||||
<Fragment key="export-contact-point">
|
||||
<Menu.Item
|
||||
icon="download-alt"
|
||||
label="Export"
|
||||
disabled={!exportAllowed}
|
||||
data-testid="export"
|
||||
onClick={() => openExportDrawer(name)}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (deleteSupported) {
|
||||
menuActions.push(
|
||||
<ConditionalWrap
|
||||
key="delete-contact-point"
|
||||
shouldWrap={isReferencedByPolicies}
|
||||
wrap={(children) => (
|
||||
<Tooltip content="Contact point is currently in use by one or more notification policies" placement="top">
|
||||
<span>{children}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
label="Delete"
|
||||
icon="trash-alt"
|
||||
destructive
|
||||
disabled={disabled || !canDelete}
|
||||
onClick={() => onDelete(name)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
@ -282,115 +358,70 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => {
|
||||
{name}
|
||||
</Text>
|
||||
</Stack>
|
||||
{isReferencedByPolicies ? (
|
||||
{isReferencedByPolicies && (
|
||||
<MetaText>
|
||||
<Link to={createUrl('/alerting/routes', { contactPoint: name })}>
|
||||
is used by <Strong>{policies}</Strong> {pluralize('notification policy', policies)}
|
||||
</Link>
|
||||
</MetaText>
|
||||
) : (
|
||||
<UnusedContactPointBadge />
|
||||
)}
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
{!isReferencedByPolicies && <UnusedContactPointBadge />}
|
||||
<Spacer />
|
||||
<LinkButton
|
||||
tooltipPlacement="top"
|
||||
tooltip={provisioned ? 'Provisioned contact points cannot be edited in the UI' : undefined}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={provisioned ? 'document-info' : 'edit'}
|
||||
icon={canEdit ? 'pen' : 'eye'}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-label={`${provisioned ? 'view' : 'edit'}-action`}
|
||||
data-testid={`${provisioned ? 'view' : 'edit'}-action`}
|
||||
aria-label={`${canEdit ? 'edit' : 'view'}-action`}
|
||||
data-testid={`${canEdit ? 'edit' : 'view'}-action`}
|
||||
href={`/alerting/notifications/receivers/${encodeURIComponent(name)}/edit`}
|
||||
>
|
||||
{provisioned ? 'View' : 'Edit'}
|
||||
{canEdit ? 'Edit' : 'View'}
|
||||
</LinkButton>
|
||||
{/* TODO probably want to split this off since there's lots of RBAC involved here */}
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{canExport && (
|
||||
<>
|
||||
<Menu.Item
|
||||
icon="download-alt"
|
||||
label={isOrgAdmin() ? 'Export' : 'Export redacted'}
|
||||
disabled={!allowedToExport}
|
||||
url={createUrl(`/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: isOrgAdmin().toString(),
|
||||
name: name,
|
||||
})}
|
||||
target="_blank"
|
||||
data-testid="export"
|
||||
/>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<ConditionalWrap
|
||||
shouldWrap={policies > 0}
|
||||
wrap={(children) => (
|
||||
<Tooltip
|
||||
content={'Contact point is currently in use by one or more notification policies'}
|
||||
placement="top"
|
||||
>
|
||||
<span>{children}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Menu.Item
|
||||
label="Delete"
|
||||
icon="trash-alt"
|
||||
destructive
|
||||
disabled={disabled || provisioned || policies > 0}
|
||||
onClick={() => onDelete(name)}
|
||||
/>
|
||||
</ConditionalWrap>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon="ellipsis-h"
|
||||
type="button"
|
||||
aria-label="more-actions"
|
||||
data-testid="more-actions"
|
||||
/>
|
||||
</Dropdown>
|
||||
{menuActions.length > 0 && (
|
||||
<Dropdown overlay={<Menu>{menuActions}</Menu>}>
|
||||
<MoreButton />
|
||||
</Dropdown>
|
||||
)}
|
||||
</Stack>
|
||||
{ExportDrawer}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContactPointReceiverProps {
|
||||
name: string;
|
||||
type: GrafanaNotifierType | string;
|
||||
description?: ReactNode;
|
||||
sendingResolved?: boolean;
|
||||
diagnostics?: NotifierStatus;
|
||||
pluginMetadata?: ReceiverPluginMetadata;
|
||||
}
|
||||
|
||||
const ContactPointReceiver = (props: ContactPointReceiverProps) => {
|
||||
const { type, description, diagnostics, sendingResolved = true } = props;
|
||||
const { name, type, description, diagnostics, pluginMetadata, sendingResolved = true } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const iconName = INTEGRATION_ICONS[type];
|
||||
const hasMetadata = diagnostics !== undefined;
|
||||
|
||||
// TODO get the actual name of the type from /ngalert if grafanaManaged AM
|
||||
const receiverName = receiverTypeNames[type] ?? upperFirst(type);
|
||||
|
||||
return (
|
||||
<div className={styles.integrationWrapper}>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
{iconName && <Icon name={iconName} />}
|
||||
<Text variant="body" color="primary">
|
||||
{receiverName}
|
||||
</Text>
|
||||
{pluginMetadata ? (
|
||||
<ReceiverMetadataBadge metadata={pluginMetadata} />
|
||||
) : (
|
||||
<Text variant="body" color="primary">
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{description && (
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
@ -502,6 +533,44 @@ const ContactPointReceiverMetadataRow = ({ diagnostics, sendingResolved }: Conta
|
||||
);
|
||||
};
|
||||
|
||||
const ALL_CONTACT_POINTS = Symbol('all contact points');
|
||||
|
||||
type ExportProps = [JSX.Element | null, (receiver: string | typeof ALL_CONTACT_POINTS) => void];
|
||||
|
||||
const useExportContactPoint = (): ExportProps => {
|
||||
const [receiverName, setReceiverName] = useState<string | typeof ALL_CONTACT_POINTS | null>(null);
|
||||
const [isExportDrawerOpen, toggleShowExportDrawer] = useToggle(false);
|
||||
const [decryptSecretsSupported, decryptSecretsAllowed] = useAlertmanagerAbility(AlertmanagerAction.DecryptSecrets);
|
||||
|
||||
const canReadSecrets = decryptSecretsSupported && decryptSecretsAllowed;
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setReceiverName(null);
|
||||
toggleShowExportDrawer(false);
|
||||
}, [toggleShowExportDrawer]);
|
||||
|
||||
const handleOpen = (receiverName: string | typeof ALL_CONTACT_POINTS) => {
|
||||
setReceiverName(receiverName);
|
||||
toggleShowExportDrawer(true);
|
||||
};
|
||||
|
||||
const drawer = useMemo(() => {
|
||||
if (!receiverName || !isExportDrawerOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (receiverName === ALL_CONTACT_POINTS) {
|
||||
// use this drawer when we want to export all contact points
|
||||
return <GrafanaReceiversExporter decrypt={canReadSecrets} onClose={handleClose} />;
|
||||
} else {
|
||||
// use this one for exporting a single contact point
|
||||
return <GrafanaReceiverExporter receiverName={receiverName} decrypt={canReadSecrets} onClose={handleClose} />;
|
||||
}
|
||||
}, [canReadSecrets, isExportDrawerOpen, handleClose, receiverName]);
|
||||
|
||||
return [drawer, handleOpen];
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contactPointWrapper: css({
|
||||
borderRadius: `${theme.shape.radius.default}`,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AlertmanagerChoice, AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ReceiversStateDTO } from 'app/types';
|
||||
|
||||
import { setupMswServer } from '../../../mockApi';
|
||||
import { mockApi, setupMswServer } from '../../../mockApi';
|
||||
import { mockAlertmanagerChoiceResponse } from '../../../mocks/alertmanagerApi';
|
||||
import { grafanaNotifiersMock } from '../../../mocks/grafana-notifiers';
|
||||
|
||||
import alertmanagerMock from './alertmanager.config.mock.json';
|
||||
import receiversMock from './receivers.mock.json';
|
||||
@ -19,6 +21,19 @@ export default () => {
|
||||
// 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))
|
||||
)
|
||||
),
|
||||
// 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)))
|
||||
);
|
||||
|
||||
// this endpoint is for rendering the "additional AMs to configure" warning
|
||||
mockAlertmanagerChoiceResponse(server, {
|
||||
alertmanagersChoice: AlertmanagerChoice.Internal,
|
||||
numExternalAlertmanagers: 1,
|
||||
});
|
||||
|
||||
// mock the endpoint for contact point metadata
|
||||
mockApi(server).grafanaNotifiers(grafanaNotifiersMock);
|
||||
|
||||
return server;
|
||||
};
|
||||
|
@ -18,6 +18,8 @@ export default () => {
|
||||
),
|
||||
rest.get(`/api/datasources/proxy/uid/${MIMIR_DATASOURCE_UID}/api/v1/status/buildinfo`, (_req, res, ctx) =>
|
||||
res(ctx.status(404))
|
||||
)
|
||||
),
|
||||
// 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)))
|
||||
);
|
||||
};
|
||||
|
@ -22,6 +22,11 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "email",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications using Grafana server configured SMTP settings",
|
||||
"name": "Email",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
],
|
||||
"name": "grafana-default-email",
|
||||
@ -46,6 +51,11 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "email",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications using Grafana server configured SMTP settings",
|
||||
"name": "Email",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
],
|
||||
"name": "provisioned-contact-point",
|
||||
@ -69,6 +79,11 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "email",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications using Grafana server configured SMTP settings",
|
||||
"name": "Email",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
],
|
||||
"name": "lotsa-emails",
|
||||
@ -93,6 +108,11 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "slack",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications to Slack",
|
||||
"name": "Slack",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
{
|
||||
"disableResolveMessage": false,
|
||||
@ -111,6 +131,11 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "slack",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications to Slack",
|
||||
"name": "Slack",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
],
|
||||
"name": "Slack with multiple channels",
|
||||
|
@ -1,15 +1,31 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
|
||||
import setupGrafanaManagedServer from './__mocks__/grafanaManagedServer';
|
||||
import { useContactPointsWithStatus } from './useContactPoints';
|
||||
|
||||
describe('useContactPoints', () => {
|
||||
setupGrafanaManagedServer();
|
||||
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
});
|
||||
|
||||
it('should return contact points with status', async () => {
|
||||
const { result } = renderHook(() => useContactPointsWithStatus('grafana'), {
|
||||
wrapper: TestProvider,
|
||||
const { result } = renderHook(() => useContactPointsWithStatus(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProvider>
|
||||
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={'grafana'}>
|
||||
{children}
|
||||
</AlertmanagerProvider>
|
||||
</TestProvider>
|
||||
),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -7,48 +7,81 @@ import { produce } from 'immer';
|
||||
import { remove } from 'lodash';
|
||||
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { onCallApi } from '../../api/onCallApi';
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
|
||||
import { enhanceContactPointsWithStatus } from './utils';
|
||||
import { enhanceContactPointsWithMetadata } from './utils';
|
||||
|
||||
export const RECEIVER_STATUS_KEY = Symbol('receiver_status');
|
||||
export const RECEIVER_META_KEY = Symbol('receiver_metadata');
|
||||
export const RECEIVER_PLUGIN_META_KEY = Symbol('receiver_plugin_metadata');
|
||||
|
||||
const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds
|
||||
|
||||
/**
|
||||
* This hook will combine data from two endpoints;
|
||||
* This hook will combine data from several endpoints;
|
||||
* 1. the alertmanager config endpoint where the definition of the receivers are
|
||||
* 2. (if available) the alertmanager receiver status endpoint, currently Grafana Managed only
|
||||
* 3. (if available) additional metadata about Grafana Managed contact points
|
||||
* 4. (if available) the OnCall plugin metadata
|
||||
*/
|
||||
export function useContactPointsWithStatus(selectedAlertmanager: string) {
|
||||
const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||
export function useContactPointsWithStatus() {
|
||||
const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
|
||||
const { installed: onCallPluginInstalled = false, loading: onCallPluginStatusLoading } = usePluginBridge(
|
||||
SupportedPlugin.OnCall
|
||||
);
|
||||
|
||||
// fetch receiver status if we're dealing with a Grafana Managed Alertmanager
|
||||
const fetchContactPointsStatus = alertmanagerApi.endpoints.getContactPointsStatus.useQuery(undefined, {
|
||||
// TODO these don't seem to work since we've not called setupListeners()
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
// re-fetch status every so often for up-to-date information
|
||||
pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
|
||||
// skip fetching receiver statuses if not Grafana AM
|
||||
skip: !isGrafanaManagedAlertmanager,
|
||||
skip: !isGrafanaAlertmanager,
|
||||
});
|
||||
|
||||
// fetch notifier metadata from the Grafana API if we're using a Grafana AM – this will be used to add additional
|
||||
// metadata and canonical names to the receiver
|
||||
const fetchReceiverMetadata = alertmanagerApi.endpoints.grafanaNotifiers.useQuery(undefined, {
|
||||
skip: !isGrafanaAlertmanager,
|
||||
});
|
||||
|
||||
// if the OnCall plugin is installed, fetch its list of integrations so we can match those to the Grafana Managed contact points
|
||||
const { data: onCallIntegrations, isLoading: onCallPluginIntegrationsLoading } =
|
||||
onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, {
|
||||
skip: !onCallPluginInstalled || !isGrafanaAlertmanager,
|
||||
});
|
||||
|
||||
// fetch the latest config from the Alertmanager
|
||||
const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(
|
||||
selectedAlertmanager,
|
||||
selectedAlertmanager!,
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
selectFromResult: (result) => ({
|
||||
...result,
|
||||
contactPoints: result.data ? enhanceContactPointsWithStatus(result.data, fetchContactPointsStatus.data) : [],
|
||||
contactPoints: result.data
|
||||
? enhanceContactPointsWithMetadata(
|
||||
result.data,
|
||||
fetchContactPointsStatus.data,
|
||||
fetchReceiverMetadata.data,
|
||||
onCallPluginInstalled ? onCallIntegrations ?? [] : null
|
||||
)
|
||||
: [],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// TODO kinda yucky to combine hooks like this, better alternative?
|
||||
// we will fail silently for fetching OnCall plugin status and integrations
|
||||
const error = fetchAlertmanagerConfiguration.error ?? fetchContactPointsStatus.error;
|
||||
const isLoading = fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading;
|
||||
const isLoading =
|
||||
fetchAlertmanagerConfiguration.isLoading ||
|
||||
fetchContactPointsStatus.isLoading ||
|
||||
onCallPluginStatusLoading ||
|
||||
onCallPluginIntegrationsLoading;
|
||||
|
||||
const contactPoints = fetchAlertmanagerConfiguration.contactPoints;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { countBy, split, trim } from 'lodash';
|
||||
import { countBy, split, trim, upperFirst } from 'lodash';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
@ -7,12 +7,15 @@ import {
|
||||
GrafanaManagedReceiverConfig,
|
||||
Route,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { NotifierStatus, ReceiversStateDTO } from 'app/types';
|
||||
import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/types';
|
||||
|
||||
import { OnCallIntegrationDTO } from '../../api/onCallApi';
|
||||
import { computeInheritedTree } from '../../utils/notification-policies';
|
||||
import { extractReceivers } from '../../utils/receivers';
|
||||
import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall';
|
||||
import { getOnCallMetadata, ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata';
|
||||
|
||||
import { RECEIVER_STATUS_KEY } from './useContactPoints';
|
||||
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY, RECEIVER_STATUS_KEY } from './useContactPoints';
|
||||
|
||||
export function isProvisioned(contactPoint: GrafanaManagedContactPoint) {
|
||||
// for some reason the provenance is on the receiver and not the entire contact point
|
||||
@ -22,7 +25,7 @@ export function isProvisioned(contactPoint: GrafanaManagedContactPoint) {
|
||||
}
|
||||
|
||||
// TODO we should really add some type information to these receiver settings...
|
||||
export function getReceiverDescription(receiver: GrafanaManagedReceiverConfig): ReactNode | undefined {
|
||||
export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): ReactNode | undefined {
|
||||
switch (receiver.type) {
|
||||
case 'email': {
|
||||
const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings
|
||||
@ -40,8 +43,11 @@ export function getReceiverDescription(receiver: GrafanaManagedReceiverConfig):
|
||||
const url = receiver.settings['url'];
|
||||
return url;
|
||||
}
|
||||
case ReceiverTypes.OnCall: {
|
||||
return receiver[RECEIVER_PLUGIN_META_KEY]?.description;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
return receiver[RECEIVER_META_KEY]?.description;
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,15 +70,21 @@ function summarizeEmailAddresses(addresses: string): string {
|
||||
}
|
||||
|
||||
// Grafana Managed contact points have receivers with additional diagnostics
|
||||
export interface ReceiverConfigWithStatus extends GrafanaManagedReceiverConfig {
|
||||
export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig {
|
||||
// we're using a symbol here so we'll never have a conflict on keys for a receiver
|
||||
// we also specify that the diagnostics might be "undefined" for vanilla Alertmanager
|
||||
[RECEIVER_STATUS_KEY]?: NotifierStatus | undefined;
|
||||
[RECEIVER_META_KEY]: {
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
// optional metadata that comes from a particular plugin (like Grafana OnCall)
|
||||
[RECEIVER_PLUGIN_META_KEY]?: ReceiverPluginMetadata;
|
||||
}
|
||||
|
||||
export interface ContactPointWithStatus extends GrafanaManagedContactPoint {
|
||||
export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
||||
numberOfPolicies: number;
|
||||
grafana_managed_receiver_configs: ReceiverConfigWithStatus[];
|
||||
grafana_managed_receiver_configs: ReceiverConfigWithMetadata[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,10 +92,12 @@ export interface ContactPointWithStatus extends GrafanaManagedContactPoint {
|
||||
* 1. we iterate over all contact points
|
||||
* 2. for each contact point we "enhance" it with the status or "undefined" for vanilla Alertmanager
|
||||
*/
|
||||
export function enhanceContactPointsWithStatus(
|
||||
export function enhanceContactPointsWithMetadata(
|
||||
result: AlertManagerCortexConfig,
|
||||
status: ReceiversStateDTO[] = []
|
||||
): ContactPointWithStatus[] {
|
||||
status: ReceiversStateDTO[] = [],
|
||||
notifiers: NotifierDTO[] = [],
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | null
|
||||
): ContactPointWithMetadata[] {
|
||||
const contactPoints = result.alertmanager_config.receivers ?? [];
|
||||
|
||||
// compute the entire inherited tree before finding what notification policies are using a particular contact point
|
||||
@ -98,10 +112,17 @@ export function enhanceContactPointsWithStatus(
|
||||
return {
|
||||
...contactPoint,
|
||||
numberOfPolicies: usedContactPointsByName[contactPoint.name] ?? 0,
|
||||
grafana_managed_receiver_configs: receivers.map((receiver, index) => ({
|
||||
...receiver,
|
||||
[RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index],
|
||||
})),
|
||||
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
|
||||
const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall;
|
||||
|
||||
return {
|
||||
...receiver,
|
||||
[RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index],
|
||||
[RECEIVER_META_KEY]: getNotifierMetadata(notifiers, receiver),
|
||||
// if OnCall plugin is installed, we'll add it to the receiver's plugin metadata
|
||||
[RECEIVER_PLUGIN_META_KEY]: isOnCallReceiver ? getOnCallMetadata(onCallIntegrations, receiver) : undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -114,3 +135,12 @@ export function getUsedContactPoints(route: Route): string[] {
|
||||
|
||||
return childrenContactPoints;
|
||||
}
|
||||
|
||||
function getNotifierMetadata(notifiers: NotifierDTO[], receiver: GrafanaManagedReceiverConfig) {
|
||||
const match = notifiers.find((notifier) => notifier.type === receiver.type);
|
||||
|
||||
return {
|
||||
name: match?.name ?? upperFirst(receiver.type),
|
||||
description: match?.description,
|
||||
};
|
||||
}
|
||||
|
@ -25,9 +25,7 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on
|
||||
type: `application/${format};charset=utf-8`,
|
||||
});
|
||||
saveAs(blob, `${downloadFileName}.${format}`);
|
||||
|
||||
onClose();
|
||||
}, [textDefinition, downloadFileName, format, onClose]);
|
||||
}, [textDefinition, downloadFileName, format]);
|
||||
|
||||
const formattedTextDefinition = useMemo(() => {
|
||||
const provider = allGrafanaExportProviders[format];
|
||||
@ -49,6 +47,7 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
readOnly: true,
|
||||
}}
|
||||
|
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Select, SelectCommonProps, Text } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
RECEIVER_META_KEY,
|
||||
RECEIVER_PLUGIN_META_KEY,
|
||||
useContactPointsWithStatus,
|
||||
} from '../contact-points/useContactPoints';
|
||||
import { ReceiverConfigWithMetadata } from '../contact-points/utils';
|
||||
|
||||
export const ContactPointSelector = (props: SelectCommonProps<string>) => {
|
||||
const { contactPoints, isLoading, error } = useContactPointsWithStatus();
|
||||
|
||||
// TODO error handling
|
||||
if (error) {
|
||||
return <span>Failed to load contact points</span>;
|
||||
}
|
||||
|
||||
const options: Array<SelectableValue<string>> = contactPoints.map((contactPoint) => {
|
||||
return {
|
||||
label: contactPoint.name,
|
||||
value: contactPoint.name,
|
||||
component: () => <ReceiversSummary receivers={contactPoint.grafana_managed_receiver_configs} />,
|
||||
};
|
||||
});
|
||||
|
||||
return <Select options={options} isLoading={isLoading} {...props} />;
|
||||
};
|
||||
|
||||
interface ReceiversProps {
|
||||
receivers: ReceiverConfigWithMetadata[];
|
||||
}
|
||||
|
||||
const ReceiversSummary = ({ receivers }: ReceiversProps) => {
|
||||
return (
|
||||
<Stack direction="row" wrap={false}>
|
||||
{receivers.map((receiver, index) => (
|
||||
<Stack key={receiver.uid ?? index} direction="row" gap={0.5}>
|
||||
{receiver[RECEIVER_PLUGIN_META_KEY]?.icon && (
|
||||
<img
|
||||
width="16px"
|
||||
src={receiver[RECEIVER_PLUGIN_META_KEY]?.icon}
|
||||
alt={receiver[RECEIVER_PLUGIN_META_KEY]?.title}
|
||||
/>
|
||||
)}
|
||||
<Text key={index} variant="bodySmall" color="secondary">
|
||||
{receiver[RECEIVER_META_KEY].name ?? receiver[RECEIVER_PLUGIN_META_KEY]?.title ?? receiver.type}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { CloudReceiverForm } from './form/CloudReceiverForm';
|
||||
@ -15,6 +16,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export const EditReceiverView = ({ config, receiverName, alertManagerSourceName }: Props) => {
|
||||
const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
|
||||
const receiver = config.alertmanager_config.receivers?.find(({ name }) => name === receiverName);
|
||||
if (!receiver) {
|
||||
return (
|
||||
@ -24,9 +27,25 @@ export const EditReceiverView = ({ config, receiverName, alertManagerSourceName
|
||||
);
|
||||
}
|
||||
|
||||
const readOnly = !editSupported || !editAllowed;
|
||||
|
||||
if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME) {
|
||||
return <GrafanaReceiverForm config={config} alertManagerSourceName={alertManagerSourceName} existing={receiver} />;
|
||||
return (
|
||||
<GrafanaReceiverForm
|
||||
config={config}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
existing={receiver}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <CloudReceiverForm config={config} alertManagerSourceName={alertManagerSourceName} existing={receiver} />;
|
||||
return (
|
||||
<CloudReceiverForm
|
||||
config={config}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
existing={receiver}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { ReceiversTable } from './ReceiversTable';
|
||||
import * as receiversMeta from './grafanaAppReceivers/useReceiversMetadata';
|
||||
import { ReceiverMetadata } from './grafanaAppReceivers/useReceiversMetadata';
|
||||
import { ReceiverPluginMetadata } from './grafanaAppReceivers/useReceiversMetadata';
|
||||
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 });
|
||||
@ -101,7 +101,7 @@ describe('ReceiversTable', () => {
|
||||
jest.resetAllMocks();
|
||||
const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 };
|
||||
useGetContactPointsStateMock.mockReturnValue(emptyContactPointsState);
|
||||
useReceiversMetadata.mockReturnValue(new Map<Receiver, ReceiverMetadata>());
|
||||
useReceiversMetadata.mockReturnValue(new Map<Receiver, ReceiverPluginMetadata>());
|
||||
});
|
||||
|
||||
it('render receivers with grafana notifiers', async () => {
|
||||
|
@ -26,7 +26,7 @@ import { ActionIcon } from '../rules/ActionIcon';
|
||||
|
||||
import { ReceiversSection } from './ReceiversSection';
|
||||
import { ReceiverMetadataBadge } from './grafanaAppReceivers/ReceiverMetadataBadge';
|
||||
import { ReceiverMetadata, useReceiversMetadata } from './grafanaAppReceivers/useReceiversMetadata';
|
||||
import { ReceiverPluginMetadata, useReceiversMetadata } from './grafanaAppReceivers/useReceiversMetadata';
|
||||
import { AlertmanagerConfigHealth, useAlertmanagerConfigHealth } from './useAlertmanagerConfigHealth';
|
||||
|
||||
interface UpdateActionProps extends ActionProps {
|
||||
@ -174,7 +174,7 @@ interface ReceiverItem {
|
||||
types: string[];
|
||||
provisioned?: boolean;
|
||||
grafanaAppReceiverType?: SupportedPlugin;
|
||||
metadata?: ReceiverMetadata;
|
||||
metadata?: ReceiverPluginMetadata;
|
||||
}
|
||||
|
||||
interface NotifierStatus {
|
||||
|
@ -23,6 +23,7 @@ interface Props {
|
||||
alertManagerSourceName: string;
|
||||
config: AlertManagerCortexConfig;
|
||||
existing?: Receiver;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const defaultChannelValues: CloudChannelValues = Object.freeze({
|
||||
@ -36,7 +37,7 @@ const defaultChannelValues: CloudChannelValues = Object.freeze({
|
||||
|
||||
const cloudNotifiers = cloudNotifierTypes.map<Notifier>((n) => ({ dto: n }));
|
||||
|
||||
export const CloudReceiverForm = ({ existing, alertManagerSourceName, config }: Props) => {
|
||||
export const CloudReceiverForm = ({ existing, alertManagerSourceName, config, readOnly = false }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
@ -70,7 +71,8 @@ export const CloudReceiverForm = ({ existing, alertManagerSourceName, config }:
|
||||
|
||||
// this basically checks if we can manage the selected alert manager data source, either because it's a Grafana Managed one
|
||||
// or a Mimir-based AlertManager
|
||||
const isManageableAlertManagerDataSource = !isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
const isManageableAlertManagerDataSource =
|
||||
!readOnly ?? !isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -12,7 +12,7 @@ import { useDispatch } from 'app/types';
|
||||
import { alertmanagerApi } from '../../../api/alertmanagerApi';
|
||||
import { testReceiversAction, updateAlertManagerConfigAction } from '../../../state/actions';
|
||||
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
import {
|
||||
formChannelValuesToGrafanaChannelConfig,
|
||||
formValuesToGrafanaReceiver,
|
||||
@ -32,6 +32,7 @@ interface Props {
|
||||
alertManagerSourceName: string;
|
||||
config: AlertManagerCortexConfig;
|
||||
existing?: GrafanaManagedContactPoint;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const defaultChannelValues: GrafanaChannelValues = Object.freeze({
|
||||
@ -43,7 +44,7 @@ const defaultChannelValues: GrafanaChannelValues = Object.freeze({
|
||||
type: 'email',
|
||||
});
|
||||
|
||||
export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config }: Props) => {
|
||||
export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config, readOnly = false }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
@ -125,12 +126,8 @@ export const GrafanaReceiverForm = ({ existing, alertManagerSourceName, config }
|
||||
? (existing.grafana_managed_receiver_configs ?? []).some((item) => Boolean(item.provenance))
|
||||
: false;
|
||||
|
||||
// this basically checks if we can manage the selected alert manager data source, either because it's a Grafana Managed one
|
||||
// or a Mimir-based AlertManager
|
||||
const isManageableAlertManagerDataSource = !isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
const isEditable = isManageableAlertManagerDataSource && !hasProvisionedItems;
|
||||
const isTestable = isManageableAlertManagerDataSource || hasProvisionedItems;
|
||||
const isEditable = !readOnly && !hasProvisionedItems;
|
||||
const isTestable = !readOnly;
|
||||
|
||||
if (isLoadingNotifiers || isLoadingOnCallIntegration) {
|
||||
return <LoadingPlaceholder text="Loading notifiers..." />;
|
||||
|
@ -3,47 +3,38 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { HorizontalGroup, Icon, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Icon, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ReceiverMetadata } from './useReceiversMetadata';
|
||||
import { ReceiverPluginMetadata } from './useReceiversMetadata';
|
||||
|
||||
interface Props {
|
||||
metadata: ReceiverMetadata;
|
||||
metadata: ReceiverPluginMetadata;
|
||||
}
|
||||
|
||||
export const ReceiverMetadataBadge = ({ metadata: { icon, title, externalUrl, warning } }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<div className={styles.wrapper}>
|
||||
<HorizontalGroup align="center" spacing="xs">
|
||||
<img src={icon} alt="" height="12px" />
|
||||
<span>{title}</span>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
{externalUrl && <LinkButton icon="external-link-alt" href={externalUrl} variant="secondary" size="sm" />}
|
||||
{warning && (
|
||||
<Tooltip content={warning} theme="error">
|
||||
<Icon name="exclamation-triangle" size="lg" className={styles.warnIcon} />
|
||||
</Tooltip>
|
||||
<Stack alignItems="center" gap={0.5}>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
{warning ? (
|
||||
<Tooltip content={warning} theme="error">
|
||||
<Icon name="exclamation-triangle" className={styles.warnIcon} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<img src={icon} alt={title} height="16px" />
|
||||
)}
|
||||
<span>{title}</span>
|
||||
</Stack>
|
||||
{externalUrl && (
|
||||
<LinkButton icon="external-link-alt" href={externalUrl} target="_blank" variant="secondary" size="sm" />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
text-align: left;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
padding: 1px 4px;
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
border: 1px solid rgba(245, 95, 62, 1);
|
||||
color: rgba(245, 95, 62, 1);
|
||||
font-weight: ${theme.typography.fontWeightRegular};
|
||||
`,
|
||||
warnIcon: css`
|
||||
fill: ${theme.colors.warning.main};
|
||||
`,
|
||||
warnIcon: css({
|
||||
fill: theme.colors.warning.text,
|
||||
}),
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { onCallApi } from '../../../api/onCallApi';
|
||||
import { GrafanaManagedReceiverConfig, Receiver } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { onCallApi, OnCallIntegrationDTO } from '../../../api/onCallApi';
|
||||
import { usePluginBridge } from '../../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../../types/pluginBridges';
|
||||
import { createBridgeURL } from '../../PluginBridge';
|
||||
@ -9,9 +9,10 @@ import { createBridgeURL } from '../../PluginBridge';
|
||||
import { ReceiverTypes } from './onCall/onCall';
|
||||
import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types';
|
||||
|
||||
export interface ReceiverMetadata {
|
||||
export interface ReceiverPluginMetadata {
|
||||
icon: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
externalUrl?: string;
|
||||
warning?: string;
|
||||
}
|
||||
@ -19,46 +20,59 @@ export interface ReceiverMetadata {
|
||||
const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[SupportedPlugin.OnCall];
|
||||
const onCallReceiverTitle = 'Grafana OnCall';
|
||||
|
||||
const onCallReceiverMeta: ReceiverMetadata = {
|
||||
const onCallReceiverMeta: ReceiverPluginMetadata = {
|
||||
title: onCallReceiverTitle,
|
||||
icon: onCallReceiverICon,
|
||||
};
|
||||
|
||||
export const useReceiversMetadata = (receivers: Receiver[]): Map<Receiver, ReceiverMetadata> => {
|
||||
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, ReceiverMetadata>();
|
||||
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, {
|
||||
...onCallReceiverMeta,
|
||||
warning: 'Grafana OnCall is not enabled',
|
||||
});
|
||||
result.set(receiver, getOnCallMetadata(null, onCallReceiver));
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingOnCallIntegration = onCallIntegrations.find(
|
||||
(i) => i.integration_url === onCallReceiver.settings.url
|
||||
);
|
||||
|
||||
result.set(receiver, {
|
||||
...onCallReceiverMeta,
|
||||
externalUrl: matchingOnCallIntegration
|
||||
? createBridgeURL(SupportedPlugin.OnCall, `/integrations/${matchingOnCallIntegration.value}`)
|
||||
: undefined,
|
||||
warning: matchingOnCallIntegration ? undefined : 'OnCall Integration no longer exists',
|
||||
});
|
||||
result.set(receiver, getOnCallMetadata(onCallIntegrations, onCallReceiver));
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [isOnCallEnabled, receivers, onCallIntegrations]);
|
||||
};
|
||||
|
||||
export function getOnCallMetadata(
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | null,
|
||||
receiver: GrafanaManagedReceiverConfig
|
||||
): ReceiverPluginMetadata {
|
||||
// indication that onCall is not enabled
|
||||
if (onCallIntegrations == null) {
|
||||
return {
|
||||
...onCallReceiverMeta,
|
||||
warning: 'Grafana OnCall is not installed or is disabled',
|
||||
};
|
||||
}
|
||||
|
||||
const matchingOnCallIntegration = onCallIntegrations.find(
|
||||
(integration) => integration.integration_url === receiver.settings.url
|
||||
);
|
||||
|
||||
return {
|
||||
...onCallReceiverMeta,
|
||||
description: matchingOnCallIntegration?.display_name,
|
||||
externalUrl: matchingOnCallIntegration
|
||||
? createBridgeURL(SupportedPlugin.OnCall, `/integrations/${matchingOnCallIntegration.value}`)
|
||||
: undefined,
|
||||
warning: matchingOnCallIntegration ? undefined : 'OnCall Integration no longer exists',
|
||||
};
|
||||
}
|
||||
|
@ -13,10 +13,6 @@ const FEATURES: FeatureDescription[] = [
|
||||
name: AlertingFeature.NotificationPoliciesV2MatchingInstances,
|
||||
defaultValue: config.featureToggles.alertingNotificationsPoliciesMatchingInstances,
|
||||
},
|
||||
{
|
||||
name: AlertingFeature.ContactPointsV2,
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: AlertingFeature.DetailsViewV2,
|
||||
defaultValue: false,
|
||||
|
Loading…
Reference in New Issue
Block a user