299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
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)}`,
|
||
}),
|
||
});
|