import { css } from '@emotion/css';
import { groupBy, size, upperFirst } from 'lodash';
import { Fragment, ReactNode } from 'react';
import { GrafanaTheme2, dateTime } 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 { useDeleteContactPointModal } from 'app/features/alerting/unified/components/contact-points/components/Modals';
import { useDeleteContactPoint } from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts';
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 './constants';
import { ContactPointWithMetadata, ReceiverConfigWithMetadata, getReceiverDescription } from './utils';
interface ContactPointProps {
contactPoint: ContactPointWithMetadata;
}
export const ContactPoint = ({ contactPoint }: ContactPointProps) => {
const { grafana_managed_receiver_configs: receivers } = contactPoint;
const styles = useStyles2(getStyles);
const { selectedAlertmanager } = useAlertmanager();
const [deleteTrigger] = useDeleteContactPoint({ alertmanager: selectedAlertmanager! });
const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger.execute);
// 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_META_KEY]));
return (
showDeleteModal({
name: contactPointToDelete.id || contactPointToDelete.name,
resourceVersion: contactPointToDelete.metadata?.resourceVersion,
})
}
/>
{showFullMetadata ? (
{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 (
);
})}
) : (
)}
{DeleteModal}
);
};
interface ContactPointReceiverProps {
name: string;
type: GrafanaNotifierType | string;
description?: ReactNode;
sendingResolved?: boolean;
diagnostics?: NotifierStatus;
pluginMetadata?: ReceiverPluginMetadata;
}
const ContactPointReceiver = (props: ContactPointReceiverProps) => {
const { name, type, description, diagnostics, pluginMetadata, sendingResolved = true } = props;
const styles = useStyles2(getStyles);
const hasMetadata = diagnostics !== undefined;
return (
{hasMetadata && }
);
};
export interface ContactPointReceiverTitleRowProps {
name: string;
type: GrafanaNotifierType | string;
description?: ReactNode;
pluginMetadata?: ReceiverPluginMetadata;
}
export function ContactPointReceiverTitleRow(props: ContactPointReceiverTitleRowProps) {
const { name, type, description, pluginMetadata } = props;
const iconName = INTEGRATION_ICONS[type];
return (
{iconName && }
{pluginMetadata ? (
) : (
{name}
)}
{description && (
{description}
)}
);
}
interface ContactPointReceiverMetadata {
sendingResolved: boolean;
diagnostics: NotifierStatus;
}
type ContactPointReceiverSummaryProps = {
receivers: ReceiverConfigWithMetadata[];
limit?: number;
};
/**
* This summary is used when we're dealing with non-Grafana managed alertmanager since they
* don't have any metadata worth showing other than a summary of what types are configured for the contact point
*/
export const ContactPointReceiverSummary = ({ receivers, limit }: ContactPointReceiverSummaryProps) => {
// limit for how many integrations are rendered
const INTEGRATIONS_LIMIT = limit ?? Number.MAX_VALUE;
const countByType = groupBy(receivers, (receiver) => receiver.type);
const numberOfUniqueIntegrations = size(countByType);
const integrationsShown = Object.entries(countByType).slice(0, INTEGRATIONS_LIMIT);
const numberOfIntegrationsNotShown = numberOfUniqueIntegrations - INTEGRATIONS_LIMIT;
return (
{integrationsShown.length === 0 && (
No integrations configured
)}
{integrationsShown.map(([type, receivers], index) => {
const iconName = INTEGRATION_ICONS[type];
const receiverName = receiverTypeNames[type] ?? upperFirst(type);
const isLastItem = size(countByType) - 1 === index;
// Pick the first integration of the grouped receivers, since they should all be the same type
// e.g. if we have multiple Oncall, they _should_ all have the same plugin metadata,
// so we can just use the first one for additional display purposes
const receiver = receivers[0];
return (
{receiver[RECEIVER_PLUGIN_META_KEY]?.icon && (
)}
{iconName && }
{receiverName}
{receivers.length > 1 && ` (${receivers.length})`}
{!isLastItem && '⋅'}
);
})}
{numberOfIntegrationsNotShown > 0 && {`+${numberOfIntegrationsNotShown} more`}}
);
};
const ContactPointReceiverMetadataRow = ({ diagnostics, sendingResolved }: ContactPointReceiverMetadata) => {
const styles = useStyles2(getStyles);
const failedToSend = Boolean(diagnostics.lastNotifyAttemptError);
const lastDeliveryAttempt = dateTime(diagnostics.lastNotifyAttempt);
const lastDeliveryAttemptDuration = diagnostics.lastNotifyAttemptDuration;
const hasDeliveryAttempt = lastDeliveryAttempt.isValid();
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
)}
>
)}
);
};
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}`,
}),
metadataRow: css({
borderBottomLeftRadius: `${theme.shape.radius.default}`,
borderBottomRightRadius: `${theme.shape.radius.default}`,
}),
noIntegrationsContainer: css({
paddingTop: `${theme.spacing(1.5)}`,
paddingLeft: `${theme.spacing(1.5)}`,
}),
});