{receivers.map((receiver, index) => {
@@ -176,6 +170,11 @@ export const ContactPointReceiverSummary = ({ receivers, limit }: ContactPointRe
return (
+ {integrationsShown.length === 0 && (
+
+ No integrations configured
+
+ )}
{integrationsShown.map(([type, receivers], index) => {
const iconName = INTEGRATION_ICONS[type];
const receiverName = receiverTypeNames[type] ?? upperFirst(type);
@@ -198,7 +197,7 @@ export const ContactPointReceiverSummary = ({ receivers, limit }: ContactPointRe
{iconName && }
{receiverName}
- {receivers.length > 1 && receivers.length}
+ {receivers.length > 1 && ` (${receivers.length})`}
{!isLastItem && '⋅'}
diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
index 6366c421d9a..c33609b367f 100644
--- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx
@@ -243,6 +243,11 @@ const ContactPointsList = ({ contactPoints, search, pageSize = DEFAULT_PAGE_SIZE
const searchResults = useContactPointsSearch(contactPoints, search);
const { page, pageItems, numberOfPages, onPageChange } = usePagination(searchResults, 1, pageSize);
+ if (pageItems.length === 0) {
+ const emptyMessage = t('alerting.contact-points.no-contact-points-found', 'No contact points found');
+ return ;
+ }
+
return (
<>
{pageItems.map((contactPoint, index) => {
diff --git a/public/app/features/alerting/unified/components/contact-points/permissions.ts b/public/app/features/alerting/unified/components/contact-points/permissions.ts
index 30af528e6dc..48ba46a10c5 100644
--- a/public/app/features/alerting/unified/components/contact-points/permissions.ts
+++ b/public/app/features/alerting/unified/components/contact-points/permissions.ts
@@ -5,7 +5,7 @@ import { AccessControlAction } from 'app/types';
*
* Any permission in this list will be checked for client side access to view Contact Points functionality.
*/
-const PERMISSIONS_CONTACT_POINTS_READ = [AccessControlAction.AlertingReceiversRead];
+export const PERMISSIONS_CONTACT_POINTS_READ = [AccessControlAction.AlertingReceiversRead];
/**
* List of granular permissions that allow modifying contact points
diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts
index 5f6deb9c0b0..c02bfabfb1a 100644
--- a/public/app/features/alerting/unified/components/contact-points/utils.ts
+++ b/public/app/features/alerting/unified/components/contact-points/utils.ts
@@ -129,7 +129,7 @@ export function enhanceContactPointsWithMetadata({
const receivers = extractReceivers(contactPoint);
const statusForReceiver = status.find((status) => status.name === contactPoint.name);
- const id = 'id' in contactPoint && contactPoint.id ? contactPoint.id : contactPoint.name;
+ const id = getContactPointIdentifier(contactPoint);
return {
...contactPoint,
@@ -157,6 +157,10 @@ export function enhanceContactPointsWithMetadata({
return enhanced.sort((a, b) => a.name.localeCompare(b.name));
}
+function getContactPointIdentifier(contactPoint: Receiver): string {
+ return 'id' in contactPoint && contactPoint.id ? contactPoint.id : contactPoint.name;
+}
+
export function isAutoGeneratedPolicy(route: Route) {
const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false;
if (!simplifiedRoutingToggleEnabled) {
diff --git a/public/app/features/alerting/unified/components/mute-timings/permissions.ts b/public/app/features/alerting/unified/components/mute-timings/permissions.ts
new file mode 100644
index 00000000000..dc895c100c8
--- /dev/null
+++ b/public/app/features/alerting/unified/components/mute-timings/permissions.ts
@@ -0,0 +1,11 @@
+import { AccessControlAction } from 'app/types';
+
+/**
+ * List of granular permissions that allow viewing contact points
+ */
+export const PERMISSIONS_TIME_INTERVALS_READ = [AccessControlAction.AlertingTimeIntervalsRead];
+
+/**
+ * List of granular permissions that allow modifying time intervals
+ */
+export const PERMISSIONS_TIME_INTERVALS_MODIFY = [AccessControlAction.AlertingTimeIntervalsWrite];
diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx
index 2985c7e20a6..19afef0f8e0 100644
--- a/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx
+++ b/public/app/features/alerting/unified/components/notification-policies/Policy.test.tsx
@@ -15,7 +15,7 @@ import {
import { ReceiversState } from 'app/types/alerting';
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
-import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks';
+import { mockReceiversState } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
@@ -57,7 +57,8 @@ describe('Policy', () => {
renderPolicy(
{
renderPolicy(
{
renderPolicy(
{
renderPolicy(
{
renderPolicy(
{
routes: [{ id: '1', object_matchers: [['foo', eq, 'bar']] }],
};
- const matchingGroups: AlertmanagerGroup[] = [
- mockAlertGroup({
- labels: {},
- alerts: [mockAlertmanagerAlert({ labels: { foo: 'bar' } }), mockAlertmanagerAlert({ labels: { foo: 'bar' } })],
- }),
- mockAlertGroup({
- labels: {},
- alerts: [mockAlertmanagerAlert({ labels: { bar: 'baz' } })],
- }),
- ];
-
renderPolicy(
{
renderPolicy(
void;
@@ -79,6 +77,7 @@ interface PolicyComponentProps {
formatter?: MatcherFormatter
) => void;
isAutoGenerated?: boolean;
+ isDefaultPolicy?: boolean;
}
const Policy = (props: PolicyComponentProps) => {
@@ -87,10 +86,8 @@ const Policy = (props: PolicyComponentProps) => {
contactPointsState,
readOnly = false,
provisioned = false,
- alertGroups = [],
alertManagerSourceName,
currentRoute,
- routeTree,
inheritedProperties,
routesMatchingFilters = {
filtersApplied: false,
@@ -102,12 +99,11 @@ const Policy = (props: PolicyComponentProps) => {
onDeletePolicy,
onShowAlertInstances,
isAutoGenerated = false,
+ isDefaultPolicy = false,
} = props;
const styles = useStyles2(getStyles);
- const isDefaultPolicy = currentRoute === routeTree;
-
const contactPoint = currentRoute.receiver;
const continueMatching = currentRoute.continue ?? false;
@@ -353,7 +349,6 @@ const Policy = (props: PolicyComponentProps) => {
return (
{
onDeletePolicy={onDeletePolicy}
onShowAlertInstances={onShowAlertInstances}
alertManagerSourceName={alertManagerSourceName}
- alertGroups={alertGroups}
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={matchingInstancesPreview}
isAutoGenerated={isThisChildAutoGenerated}
@@ -844,7 +838,6 @@ interface ContactPointDetailsProps {
receivers: Receiver[];
}
-// @TODO make this work for cloud AMs too
const ContactPointsHoverDetails: FC = ({
alertManagerSourceName,
contactPoint,
@@ -852,67 +845,40 @@ const ContactPointsHoverDetails: FC = ({
}) => {
const details = receivers.find((receiver) => receiver.name === contactPoint);
if (!details) {
+ // If we can't find details, then it's possible (likely) that the user doesn't have access to this
+ // contact point, so we don't try and link to it
return (
-
+
{contactPoint}
-
+
);
}
const integrations = details.grafana_managed_receiver_configs;
- if (!integrations) {
- return (
-
- {contactPoint}
-
- );
- }
- const groupedIntegrations = groupBy(details.grafana_managed_receiver_configs, (config) => config.type);
+ const contactPointLink =
+ 'id' in details && details.id
+ ? createContactPointLink(details.id, alertManagerSourceName)
+ : createContactPointSearchLink(details.name, alertManagerSourceName);
return (
-
- Contact Point
-
{contactPoint}
}
key={uniqueId()}
content={
-
- {/* use "label" to indicate how many of that type we have in the contact point */}
- {Object.entries(groupedIntegrations).map(([type, integrations]) => (
-
+
+
+
}
>
-
+
{contactPoint}
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
index 42e0bc67804..8d3dfdae77a 100644
--- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
@@ -9,7 +9,7 @@ import { AlertmanagerAction } from '../../../hooks/useAbilities';
import { AlertmanagerProvider } from '../../../state/AlertmanagerContext';
import { getAmMatcherFormatter } from '../../../utils/alertmanager';
import { MatcherFormatter } from '../../../utils/matchers';
-import { makeAMLink } from '../../../utils/misc';
+import { createContactPointSearchLink } from '../../../utils/misc';
import { Authorize } from '../../Authorize';
import { Matchers } from '../../notification-policies/Matchers';
@@ -98,10 +98,7 @@ export function NotificationRouteDetailsModal({
{
hasConfigurationAPI,
notificationsPermissions.create,
// TODO: Move this into the permissions config and generalise that code to allow for an array of permissions
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversCreate : null
+ ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversCreate] : [])
),
[AlertmanagerAction.ViewContactPoint]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversRead : null
+ ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_CONTACT_POINTS_READ : [])
),
[AlertmanagerAction.UpdateContactPoint]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
+ ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : [])
),
[AlertmanagerAction.DeleteContactPoint]: toAbility(
hasConfigurationAPI,
notificationsPermissions.delete,
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
+ ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : [])
),
// At the time of writing, only Grafana flavored alertmanager supports exporting,
// and if a user can view the contact point, then they can also export it
@@ -254,17 +259,17 @@ export function useAllAlertmanagerAbilities(): Abilities {
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(
hasConfigurationAPI,
notificationsPermissions.create,
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
+ ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : [])
),
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesRead : null
+ ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesRead] : [])
),
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
- isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
+ ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : [])
),
[AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
// -- notification policies --
@@ -287,11 +292,27 @@ export function useAllAlertmanagerAbilities(): Abilities {
[AlertmanagerAction.ViewSilence]: toAbility(AlwaysSupported, instancePermissions.read),
[AlertmanagerAction.UpdateSilence]: toAbility(AlwaysSupported, instancePermissions.update),
[AlertmanagerAction.PreviewSilencedInstances]: toAbility(AlwaysSupported, instancePermissions.read),
- // -- mute timtings --
- [AlertmanagerAction.CreateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
- [AlertmanagerAction.ViewMuteTiming]: toAbility(AlwaysSupported, notificationsPermissions.read),
- [AlertmanagerAction.UpdateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
- [AlertmanagerAction.DeleteMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
+ // -- mute timings --
+ [AlertmanagerAction.CreateMuteTiming]: toAbility(
+ hasConfigurationAPI,
+ notificationsPermissions.create,
+ ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
+ ),
+ [AlertmanagerAction.ViewMuteTiming]: toAbility(
+ AlwaysSupported,
+ notificationsPermissions.read,
+ ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_READ : [])
+ ),
+ [AlertmanagerAction.UpdateMuteTiming]: toAbility(
+ hasConfigurationAPI,
+ notificationsPermissions.update,
+ ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
+ ),
+ [AlertmanagerAction.DeleteMuteTiming]: toAbility(
+ hasConfigurationAPI,
+ notificationsPermissions.delete,
+ ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
+ ),
[AlertmanagerAction.ExportMuteTimings]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
};
@@ -355,7 +376,8 @@ function useCanSilence(rule: CombinedRule): [boolean, boolean] {
}
// just a convenient function
-const toAbility = (supported: boolean, ...actions: Array): Ability => [
- supported,
- actions.some((action) => action && ctx.hasPermission(action)),
-];
+const toAbility = (
+ supported: boolean,
+ /** If user has any of these permissions, then they are allowed to perform the action */
+ ...actions: AccessControlAction[]
+): Ability => [supported, actions.some((action) => action && ctx.hasPermission(action))];
diff --git a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx
index 3e89460e872..741a62fbe35 100644
--- a/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx
+++ b/public/app/features/alerting/unified/rule-list/components/AlertRuleListItem.tsx
@@ -15,7 +15,7 @@ import { ProvisioningBadge } from '../../components/Provisioning';
import { PluginOriginBadge } from '../../plugins/PluginOriginBadge';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { labelsSize } from '../../utils/labels';
-import { createContactPointLink } from '../../utils/misc';
+import { createContactPointSearchLink } from '../../utils/misc';
import { RulePluginOrigin } from '../../utils/rules';
import { ListItem } from './ListItem';
@@ -106,7 +106,7 @@ export const AlertRuleListItem = (props: AlertRuleListItemProps) => {
Delivered to{' '}
only firing1> notifications",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index bc936363a98..df94a7497f3 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -137,7 +137,6 @@
"export-all": "Ēχpőřŧ äľľ",
"view": "Vįęŵ"
},
- "contact-point": "Cőʼnŧäčŧ Pőįʼnŧ",
"contact-points": {
"create": "Cřęäŧę čőʼnŧäčŧ pőįʼnŧ",
"custom-template-value": "Cūşŧőm ŧęmpľäŧę väľūę",
@@ -155,6 +154,7 @@
},
"last-delivery-attempt": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ",
"last-delivery-failed": "Ŀäşŧ đęľįvęřy äŧŧęmpŧ ƒäįľęđ",
+ "no-contact-points-found": "Ńő čőʼnŧäčŧ pőįʼnŧş ƒőūʼnđ",
"no-delivery-attempts": "Ńő đęľįvęřy äŧŧęmpŧş",
"no-integrations": "Ńő įʼnŧęģřäŧįőʼnş čőʼnƒįģūřęđ",
"only-firing": "Đęľįvęřįʼnģ <1>őʼnľy ƒįřįʼnģ1> ʼnőŧįƒįčäŧįőʼnş",