diff --git a/.betterer.results b/.betterer.results
index ac00f9cbff6..ba808d11d27 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1706,21 +1706,10 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with ", "2"]
],
"public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx:5381": [
- [0, 0, 0, "Do not use any type assertions.", "0"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "2"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "3"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "4"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "5"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "6"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "7"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "8"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "9"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "10"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "11"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "12"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "13"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "14"]
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "3"]
],
"public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
diff --git a/public/app/features/alerting/unified/components/common/TextVariants.tsx b/public/app/features/alerting/unified/components/common/TextVariants.tsx
new file mode 100644
index 00000000000..3842c2b8287
--- /dev/null
+++ b/public/app/features/alerting/unified/components/common/TextVariants.tsx
@@ -0,0 +1,8 @@
+// These are convenience components to deal with i18n shenanigans
+// (see https://github.com/grafana/grafana/blob/main/contribute/internationalization.md#jsx)
+// These help when we need to interpolate variables inside translated strings,
+// where we need to style them differently
+
+import { Text } from '@grafana/ui';
+
+export const PrimaryText = ({ content }: { content: string }) => {content};
diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx
new file mode 100644
index 00000000000..5f94195df4b
--- /dev/null
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx
@@ -0,0 +1,272 @@
+import { css } from '@emotion/css';
+import { groupBy, size, upperFirst } from 'lodash';
+import { Fragment, ReactNode } from 'react';
+
+import { dateTime, GrafanaTheme2 } from '@grafana/data';
+import { Icon, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
+import { Trans } from 'app/core/internationalization';
+import { PrimaryText } from 'app/features/alerting/unified/components/common/TextVariants';
+import { ContactPointHeader } from 'app/features/alerting/unified/components/contact-points/ContactPointHeader';
+import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts';
+import { GrafanaManagedReceiverConfig } from 'app/plugins/datasource/alertmanager/types';
+import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting';
+
+import { INTEGRATION_ICONS } from '../../types/contact-points';
+import { MetaText } from '../MetaText';
+import { ReceiverMetadataBadge } from '../receivers/grafanaAppReceivers/ReceiverMetadataBadge';
+import { ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata';
+
+import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY, RECEIVER_STATUS_KEY } from './useContactPoints';
+import { getReceiverDescription, ReceiverConfigWithMetadata, RouteReference } from './utils';
+
+interface ContactPointProps {
+ name: string;
+ disabled?: boolean;
+ provisioned?: boolean;
+ receivers: ReceiverConfigWithMetadata[];
+ policies?: RouteReference[];
+ onDelete: (name: string) => void;
+}
+
+export const ContactPoint = ({
+ name,
+ disabled = false,
+ provisioned = false,
+ receivers,
+ policies = [],
+ onDelete,
+}: ContactPointProps) => {
+ const styles = useStyles2(getStyles);
+
+ // TODO probably not the best way to figure out if we want to show either only the summary or full metadata for the receivers?
+ const showFullMetadata = receivers.some((receiver) => Boolean(receiver[RECEIVER_STATUS_KEY]));
+
+ return (
+
+
+ {/* this is shown when the last delivery failed – we don't show any additional metadata */}
+ {failedToSend ? (
+ <>
+
+
+
+ Last delivery attempt failed
+
+
+
+ >
+ ) : (
+ <>
+ {/* this is shown when we have a last delivery attempt */}
+ {hasDeliveryAttempt && (
+ <>
+
+ Last delivery attempt
+
+
+ {lastDeliveryAttempt.locale('en').fromNow()}
+
+
+
+
+
+ Last delivery took
+
+
+ >
+ )}
+ {/* when we have no last delivery attempt */}
+ {!hasDeliveryAttempt && (
+
+ No delivery attempts
+
+ )}
+ {/* this is only shown for contact points that only want "firing" updates */}
+ {!sendingResolved && (
+
+
+ Delivering only firing notifications
+
+
+ )}
+ >
+ )}
+
+
-
- {/* this is shown when the last delivery failed – we don't show any additional metadata */}
- {failedToSend ? (
- <>
-
-
- Last delivery attempt failed
-
-
- >
- ) : (
- <>
- {/* this is shown when we have a last delivery attempt */}
- {hasDeliveryAttempt && (
- <>
-
- Last delivery attempt{' '}
-
-
- {lastDeliveryAttempt.locale('en').fromNow()}
-
-
-
-
- took {lastDeliveryAttemptDuration}
-
- >
- )}
- {/* when we have no last delivery attempt */}
- {!hasDeliveryAttempt && No delivery attempts}
- {/* this is only shown for contact points that only want "firing" updates */}
- {!sendingResolved && (
-
- Delivering only firing notifications
-
- )}
- >
- )}
-
-
- );
-};
-
-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(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 ;
- } else {
- // use this one for exporting a single contact point
- return ;
- }
- }, [canReadSecrets, isExportDrawerOpen, handleClose, receiverName]);
-
- return [drawer, handleOpen];
-};
-
-const getStyles = (theme: GrafanaTheme2) => ({
- contactPointWrapper: css({
- borderRadius: `${theme.shape.radius.default}`,
- border: `solid 1px ${theme.colors.border.weak}`,
- borderBottom: 'none',
- }),
- integrationWrapper: css({
- position: 'relative',
-
- background: `${theme.colors.background.primary}`,
- padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
-
- borderBottom: `solid 1px ${theme.colors.border.weak}`,
- }),
- headerWrapper: css({
- background: `${theme.colors.background.secondary}`,
- padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
-
- borderBottom: `solid 1px ${theme.colors.border.weak}`,
- borderTopLeftRadius: `${theme.shape.radius.default}`,
- borderTopRightRadius: `${theme.shape.radius.default}`,
- }),
- metadataRow: css({
- borderBottomLeftRadius: `${theme.shape.radius.default}`,
- borderBottomRightRadius: `${theme.shape.radius.default}`,
- }),
-});
-
-export default ContactPoints;
+export default ContactPointsPageContents;
diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPointsSearch.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPointsSearch.tsx
new file mode 100644
index 00000000000..397bfb3087e
--- /dev/null
+++ b/public/app/features/alerting/unified/components/contact-points/useContactPointsSearch.tsx
@@ -0,0 +1,42 @@
+import uFuzzy from '@leeoniya/ufuzzy';
+import { uniq } from 'lodash';
+import { useMemo } from 'react';
+
+import { RECEIVER_META_KEY } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
+import { ContactPointWithMetadata } from 'app/features/alerting/unified/components/contact-points/utils';
+
+const fuzzyFinder = new uFuzzy({
+ intraMode: 1,
+ intraIns: 1,
+ intraSub: 1,
+ intraDel: 1,
+ intraTrn: 1,
+});
+
+// let's search in two different haystacks, the name of the contact point and the type of the receiver(s)
+export const useContactPointsSearch = (
+ contactPoints: ContactPointWithMetadata[],
+ search?: string | null
+): ContactPointWithMetadata[] => {
+ const nameHaystack = useMemo(() => {
+ return contactPoints.map((contactPoint) => contactPoint.name);
+ }, [contactPoints]);
+
+ const typeHaystack = useMemo(() => {
+ return contactPoints.map((contactPoint) =>
+ // we're using the resolved metadata key here instead of the "type" property – ex. we alias "teams" to "microsoft teams"
+ contactPoint.grafana_managed_receiver_configs.map((receiver) => receiver[RECEIVER_META_KEY].name).join(' ')
+ );
+ }, [contactPoints]);
+
+ if (!search) {
+ return contactPoints;
+ }
+
+ const nameHits = fuzzyFinder.filter(nameHaystack, search) ?? [];
+ const typeHits = fuzzyFinder.filter(typeHaystack, search) ?? [];
+
+ const hits = [...nameHits, ...typeHits];
+
+ return uniq(hits).map((id) => contactPoints[id]) ?? [];
+};
diff --git a/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx b/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx
new file mode 100644
index 00000000000..3dcb8753ccc
--- /dev/null
+++ b/public/app/features/alerting/unified/components/contact-points/useExportContactPoint.tsx
@@ -0,0 +1,44 @@
+import { useCallback, useMemo, useState } from 'react';
+import { useToggle } from 'react-use';
+
+import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
+import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter';
+import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter';
+
+export const ALL_CONTACT_POINTS = Symbol('all contact points');
+
+type ExportProps = [JSX.Element | null, (receiver: string | typeof ALL_CONTACT_POINTS) => void];
+
+export const useExportContactPoint = (): ExportProps => {
+ const [receiverName, setReceiverName] = useState(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 ;
+ } else {
+ // use this one for exporting a single contact point
+ return ;
+ }
+ }, [canReadSecrets, isExportDrawerOpen, handleClose, receiverName]);
+
+ return [drawer, handleOpen];
+};
diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx
index 194bcfd5679..b4239207f8f 100644
--- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx
+++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx
@@ -23,6 +23,7 @@ import {
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import ConditionalWrap from 'app/features/alerting/unified/components/ConditionalWrap';
+import { PrimaryText } from 'app/features/alerting/unified/components/common/TextVariants';
import {
AlertmanagerGroup,
MatcherOperator,
@@ -1021,8 +1022,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
}),
});
-// This is a convencience component to deal with I18n shenanigans
-// see https://github.com/grafana/grafana/blob/main/contribute/internationalization.md#jsx
-const PrimaryText = ({ content }: { content: string }) => {content};
-
export { Policy };
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
index 55751a9db97..5dd38c4c1bd 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
@@ -7,7 +7,7 @@ import { Alert, CollapsableSection, LoadingPlaceholder, Stack, useStyles2 } from
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource';
-import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints';
+import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoint';
import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints';
import { ContactPointWithMetadata } from '../../../contact-points/utils';
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx
index 6b2a949117b..5d164e547a5 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointDetails.tsx
@@ -1,6 +1,6 @@
import { Stack } from '@grafana/ui';
-import { ContactPointReceiverTitleRow } from '../../../../contact-points/ContactPoints';
+import { ContactPointReceiverTitleRow } from '../../../../contact-points/ContactPoint';
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from '../../../../contact-points/useContactPoints';
import { ReceiverConfigWithMetadata, getReceiverDescription } from '../../../../contact-points/utils';
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index d9637c637f2..d554908eb79 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -100,10 +100,17 @@
}
},
"contact-points": {
+ "delivery-duration": "Last delivery took <1>1>",
+ "last-delivery-attempt": "Last delivery attempt",
+ "last-delivery-failed": "Last delivery attempt failed",
+ "no-delivery-attempts": "No delivery attempts",
+ "only-firing": "Delivering <1>only firing1> notifications",
"telegram": {
"parse-mode-warning-body": "If you use a <1>parse_mode1> option other than <3>None3>, truncation may result in an invalid message, causing the notification to fail. For longer messages, we recommend using an alternative contact method.",
"parse-mode-warning-title": "Telegram messages are limited to 4096 UTF-8 characters."
- }
+ },
+ "used-by_one": "Used by {{ count }} notification policy",
+ "used-by_other": "Used by {{ count }} notification policies"
},
"policies": {
"metadata": {
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 10a4ee051e7..cf684d502de 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -100,10 +100,17 @@
}
},
"contact-points": {
+ "delivery-duration": "Ŀäşŧ đęľįvęřy ŧőőĸ <1>1>",
+ "last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ",
+ "last-delivery-failed": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ ƒäįľęđ",
+ "no-delivery-attempts": "Ńő đęľįvęřy äŧŧęmpŧş",
+ "only-firing": "Đęľįvęřįʼnģ <1>őʼnľy ƒįřįʼnģ1> ʼnőŧįƒįčäŧįőʼnş",
"telegram": {
"parse-mode-warning-body": "Ĩƒ yőū ūşę ä <1>päřşę_mőđę1> őpŧįőʼn őŧĥęř ŧĥäʼn <3>Ńőʼnę3>, ŧřūʼnčäŧįőʼn mäy řęşūľŧ įʼn äʼn įʼnväľįđ męşşäģę, čäūşįʼnģ ŧĥę ʼnőŧįƒįčäŧįőʼn ŧő ƒäįľ. Főř ľőʼnģęř męşşäģęş, ŵę řęčőmmęʼnđ ūşįʼnģ äʼn äľŧęřʼnäŧįvę čőʼnŧäčŧ męŧĥőđ.",
"parse-mode-warning-title": "Ŧęľęģřäm męşşäģęş äřę ľįmįŧęđ ŧő 4096 ŮŦF-8 čĥäřäčŧęřş."
- }
+ },
+ "used-by_one": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčy",
+ "used-by_other": "Ůşęđ þy {{ count }} ʼnőŧįƒįčäŧįőʼn pőľįčįęş"
},
"policies": {
"metadata": {