import { css } from '@emotion/css'; import { useMemo } from 'react'; import { GrafanaTheme2, dateMath } from '@grafana/data'; import { Alert, CollapsableSection, Divider, Icon, Link, LinkButton, LoadingPlaceholder, Stack, useStyles2, } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { Trans } from 'app/core/internationalization'; import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi'; import { featureDiscoveryApi } from 'app/features/alerting/unified/api/featureDiscoveryApi'; import { MATCHER_ALERT_RULE_UID, SILENCES_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; import { GRAFANA_RULES_SOURCE_NAME, getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource'; import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers'; import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc'; import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { Authorize } from '../Authorize'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning'; import { Matchers } from './Matchers'; import { NoSilencesSplash } from './NoSilencesCTA'; import { SilenceDetails } from './SilenceDetails'; import { SilenceStateTag } from './SilenceStateTag'; import { SilencesFilter } from './SilencesFilter'; export interface SilenceTableItem extends Silence { silencedAlerts: AlertmanagerAlert[] | undefined; } type SilenceTableColumnProps = DynamicTableColumnProps; type SilenceTableItemProps = DynamicTableItemProps; const API_QUERY_OPTIONS = { pollingInterval: SILENCES_POLL_INTERVAL_MS, refetchOnFocus: true }; const SilencesTable = () => { const { selectedAlertmanager: alertManagerSourceName = '' } = useAlertmanager(); const [previewAlertsSupported, previewAlertsAllowed] = useAlertmanagerAbility( AlertmanagerAction.PreviewSilencedInstances ); const canPreview = previewAlertsSupported && previewAlertsAllowed; const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } = alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery( { amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } }, { ...API_QUERY_OPTIONS, skip: !canPreview } ); const { data: silences = [], isLoading, error, } = alertSilencesApi.endpoints.getSilences.useQuery( { datasourceUid: getDatasourceAPIUid(alertManagerSourceName), ruleMetadata: true, accessControl: true }, API_QUERY_OPTIONS ); const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery( { amSourceName: alertManagerSourceName ?? '' }, { skip: !alertManagerSourceName } ); const mimirLazyInitError = stringifyErrorLike(error).includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit; const styles = useStyles2(getStyles); const [queryParams] = useQueryParams(); const filteredSilencesNotExpired = useFilteredSilences(silences, false); const filteredSilencesExpired = useFilteredSilences(silences, true); const { silenceState: silenceStateInParams } = getSilenceFiltersFromUrlParams(queryParams); const showExpiredFromUrl = silenceStateInParams === SilenceState.Expired; const itemsNotExpired = useMemo((): SilenceTableItemProps[] => { const findSilencedAlerts = (id: string) => { return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id)); }; return filteredSilencesNotExpired.map((silence) => { const silencedAlerts = canPreview ? findSilencedAlerts(silence.id) : undefined; return { id: silence.id, data: { ...silence, silencedAlerts }, }; }); }, [filteredSilencesNotExpired, alertManagerAlerts, canPreview]); const itemsExpired = useMemo((): SilenceTableItemProps[] => { const findSilencedAlerts = (id: string) => { return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id)); }; return filteredSilencesExpired.map((silence) => { const silencedAlerts = canPreview ? findSilencedAlerts(silence.id) : undefined; return { id: silence.id, data: { ...silence, silencedAlerts }, }; }); }, [filteredSilencesExpired, alertManagerAlerts, canPreview]); if (isLoading || amAlertsIsLoading) { return ; } if (mimirLazyInitError) { return ( Create a new contact point to create a configuration using the default values or contact your administrator to set up the Alertmanager. ); } if (error) { const errMessage = stringifyErrorLike(error) || 'Unknown error.'; return ( {errMessage} ); } return (
{!!silences.length && ( Add Silence {itemsExpired.length > 0 && (
Expired silences are automatically deleted after 5 days.
)}
)} {!silences.length && }
); }; function SilenceList({ items, alertManagerSourceName, dataTestId, }: { items: SilenceTableItemProps[]; alertManagerSourceName: string; dataTestId: string; }) { const columns = useColumns(alertManagerSourceName); if (!!items.length) { return ( { return ( <> ); }} /> ); } else { return No matching silences found;; } } const useFilteredSilences = (silences: Silence[], expired = false) => { const [queryParams] = useQueryParams(); return useMemo(() => { const { queryString } = getSilenceFiltersFromUrlParams(queryParams); const silenceIdsString = queryParams?.silenceIds; return silences.filter((silence) => { if (typeof silenceIdsString === 'string') { const idsIncluded = silenceIdsString.split(',').includes(silence.id); if (!idsIncluded) { return false; } } if (queryString) { const matchers = parsePromQLStyleMatcherLooseSafe(queryString); const matchersMatch = matchers.every((matcher) => silence.matchers?.some( ({ name, value, isEqual, isRegex }) => matcher.name === name && matcher.value === value && matcher.isEqual === isEqual && matcher.isRegex === isRegex ) ); if (!matchersMatch) { return false; } } if (expired) { return silence.status.state === SilenceState.Expired; } else { return silence.status.state !== SilenceState.Expired; } }); }, [queryParams, silences, expired]); }; const getStyles = (theme: GrafanaTheme2) => ({ callout: css({ backgroundColor: theme.colors.background.secondary, borderTop: `3px solid ${theme.colors.info.border}`, borderRadius: theme.shape.radius.default, height: '62px', display: 'flex', flexDirection: 'row', alignItems: 'center', '& > *': { marginLeft: theme.spacing(1), }, }), calloutIcon: css({ color: theme.colors.info.text, }), }); function useColumns(alertManagerSourceName: string) { const [updateSupported, updateAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateSilence); const [expireSilence] = alertSilencesApi.endpoints.expireSilence.useMutation(); const isGrafanaFlavoredAlertmanager = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; return useMemo((): SilenceTableColumnProps[] => { const handleExpireSilenceClick = (silenceId: string) => { expireSilence({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName), silenceId }); }; const columns: SilenceTableColumnProps[] = [ { id: 'state', label: 'State', renderCell: function renderStateTag({ data: { status } }) { return ; }, size: 3, }, { id: 'alert-rule', label: 'Alert rule targeted', renderCell: function renderAlertRuleLink({ data: { metadata } }) { return metadata?.rule_title ? ( {metadata.rule_title} ) : ( 'None' ); }, size: 8, }, { id: 'matchers', label: 'Matching labels', renderCell: function renderMatchers({ data: { matchers } }) { const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || []; return ; }, size: 7, }, { id: 'alerts', label: 'Alerts silenced', renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) { return {Array.isArray(silencedAlerts) ? silencedAlerts.length : '-'}; }, size: 2, }, { id: 'schedule', label: 'Schedule', renderCell: function renderSchedule({ data: { startsAt, endsAt } }) { const startsAtDate = dateMath.parse(startsAt); const endsAtDate = dateMath.parse(endsAt); const dateDisplayFormat = 'YYYY-MM-DD HH:mm'; return `${startsAtDate?.format(dateDisplayFormat)} - ${endsAtDate?.format(dateDisplayFormat)}`; }, size: 7, }, ]; if (updateSupported) { columns.push({ id: 'actions', label: 'Actions', renderCell: function renderActions({ data: silence }) { const isExpired = silence.status.state === SilenceState.Expired; const canCreate = silence?.accessControl?.create; const canWrite = silence?.accessControl?.write; const canRecreate = isExpired && (isGrafanaFlavoredAlertmanager ? canCreate : updateAllowed); const canEdit = !isExpired && (isGrafanaFlavoredAlertmanager ? canWrite : updateAllowed); return ( {canRecreate && ( Recreate )} {canEdit && ( <> handleExpireSilenceClick(silence.id)} > Unsilence Edit )} ); }, size: 5, }); } return columns; }, [alertManagerSourceName, expireSilence, isGrafanaFlavoredAlertmanager, updateAllowed, updateSupported]); } function SilencesTablePage() { return ( ); } export default withPageErrorBoundary(SilencesTablePage);