The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx

296 lines
11 KiB

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 (
<div className={styles.contactPointWrapper} data-testid="contact-point">
<Stack direction="column" gap={0}>
<ContactPointHeader
contactPoint={contactPoint}
onDelete={(contactPointToDelete) =>
showDeleteModal({
name: contactPointToDelete.id || contactPointToDelete.name,
resourceVersion: contactPointToDelete.metadata?.resourceVersion,
})
}
/>
{showFullMetadata ? (
<div>
{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={key}
name={metadata.name}
type={receiver.type}
description={getReceiverDescription(receiver)}
diagnostics={diagnostics}
pluginMetadata={pluginMetadata}
sendingResolved={sendingResolved}
/>
);
})}
</div>
) : (
<div className={styles.integrationWrapper}>
<ContactPointReceiverSummary receivers={receivers} />
</div>
)}
</Stack>
{DeleteModal}
</div>
);
};
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 (
<div className={styles.integrationWrapper}>
<Stack direction="column" gap={0.5}>
<ContactPointReceiverTitleRow
name={name}
type={type}
description={description}
pluginMetadata={pluginMetadata}
/>
{hasMetadata && <ContactPointReceiverMetadataRow diagnostics={diagnostics} sendingResolved={sendingResolved} />}
</Stack>
</div>
);
};
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 (
<Stack direction="row" alignItems="center" gap={1}>
<Stack direction="row" alignItems="center" gap={0.5}>
{iconName && <Icon name={iconName} />}
{pluginMetadata ? (
<ReceiverMetadataBadge metadata={pluginMetadata} />
) : (
<Text variant="body" color="primary">
{name}
</Text>
)}
</Stack>
{description && (
<Text variant="bodySmall" color="secondary">
{description}
</Text>
)}
</Stack>
);
}
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 (
<Stack direction="column" gap={0}>
<Stack direction="row" alignItems="center" gap={1}>
{integrationsShown.length === 0 && (
<MetaText color="warning" icon="exclamation-triangle">
<Trans i18nKey="alerting.contact-points.no-integrations">No integrations configured</Trans>
</MetaText>
)}
{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 (
<Fragment key={type}>
<Stack direction="row" alignItems="center" gap={0.5}>
{receiver[RECEIVER_PLUGIN_META_KEY]?.icon && (
<img
width="14px"
src={receiver[RECEIVER_PLUGIN_META_KEY]?.icon}
alt={receiver[RECEIVER_PLUGIN_META_KEY]?.title}
/>
)}
{iconName && <Icon name={iconName} />}
<span>
{receiverName}
{receivers.length > 1 && ` (${receivers.length})`}
</span>
</Stack>
{!isLastItem && '⋅'}
</Fragment>
);
})}
{numberOfIntegrationsNotShown > 0 && <span>{`+${numberOfIntegrationsNotShown} more`}</span>}
</Stack>
</Stack>
);
};
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 (
<div className={styles.metadataRow}>
<Stack direction="row" gap={1}>
{/* this is shown when the last delivery failed – we don't show any additional metadata */}
{failedToSend ? (
<MetaText color="error" icon="exclamation-circle">
<Tooltip content={diagnostics.lastNotifyAttemptError!}>
<span>
<Trans i18nKey="alerting.contact-points.last-delivery-failed">Last delivery attempt failed</Trans>
</span>
</Tooltip>
</MetaText>
) : (
<>
{/* this is shown when we have a last delivery attempt */}
{hasDeliveryAttempt && (
<>
<MetaText icon="clock-nine">
<Trans i18nKey="alerting.contact-points.last-delivery-attempt">Last delivery attempt</Trans>
<Tooltip content={lastDeliveryAttempt.toLocaleString()}>
<span>
<Text color="primary">{lastDeliveryAttempt.locale('en').fromNow()}</Text>
</span>
</Tooltip>
</MetaText>
<MetaText icon="stopwatch">
<Trans i18nKey="alerting.contact-points.delivery-duration">
Last delivery took <PrimaryText content={lastDeliveryAttemptDuration} />
</Trans>
</MetaText>
</>
)}
{/* when we have no last delivery attempt */}
{!hasDeliveryAttempt && (
<MetaText icon="clock-nine">
<Trans i18nKey="alerting.contact-points.no-delivery-attempts">No delivery attempts</Trans>
</MetaText>
)}
{/* this is only shown for contact points that only want "firing" updates */}
{!sendingResolved && (
<MetaText icon="info-circle">
<Trans i18nKey="alerting.contact-points.only-firing">
Delivering <Text color="primary">only firing</Text> notifications
</Trans>
</MetaText>
)}
</>
)}
</Stack>
</div>
);
};
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)}`,
}),
});