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/silences/SilencesTable.tsx

380 lines
13 KiB

import { css } from '@emotion/css';
import React, { 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 { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi';
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 { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
import { parseMatchers } from '../../utils/alertmanager';
import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc';
import { Authorize } from '../Authorize';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
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[];
}
type SilenceTableColumnProps = DynamicTableColumnProps<SilenceTableItem>;
type SilenceTableItemProps = DynamicTableItemProps<SilenceTableItem>;
interface Props {
alertManagerSourceName: string;
}
const API_QUERY_OPTIONS = { pollingInterval: SILENCES_POLL_INTERVAL_MS, refetchOnFocus: true };
const SilencesTable = ({ alertManagerSourceName }: Props) => {
const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } =
alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery(
{ amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } },
API_QUERY_OPTIONS
);
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 = findSilencedAlerts(silence.id);
return {
id: silence.id,
data: { ...silence, silencedAlerts },
};
});
}, [filteredSilencesNotExpired, alertManagerAlerts]);
const itemsExpired = useMemo((): SilenceTableItemProps[] => {
const findSilencedAlerts = (id: string) => {
return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id));
};
return filteredSilencesExpired.map((silence) => {
const silencedAlerts = findSilencedAlerts(silence.id);
return {
id: silence.id,
data: { ...silence, silencedAlerts },
};
});
}, [filteredSilencesExpired, alertManagerAlerts]);
if (isLoading || amAlertsIsLoading) {
return <LoadingPlaceholder text="Loading silences..." />;
}
if (mimirLazyInitError) {
return (
<Alert title="The selected Alertmanager has no configuration" severity="warning">
<Trans i18nKey="silences.table.noConfig">
Create a new contact point to create a configuration using the default values or contact your administrator to
set up the Alertmanager.
</Trans>
</Alert>
);
}
if (error) {
const errMessage = stringifyErrorLike(error) || 'Unknown error.';
return (
<Alert severity="error" title="Error loading silences">
{errMessage}
</Alert>
);
}
return (
<div data-testid="silences-table">
{!!silences.length && (
<Stack direction="column">
<SilencesFilter />
<Authorize actions={[AlertmanagerAction.CreateSilence]}>
<Stack justifyContent="end">
<LinkButton href={makeAMLink('/alerting/silence/new', alertManagerSourceName)} icon="plus">
<Trans i18nKey="silences.table.add-silence-button">Add Silence</Trans>
</LinkButton>
</Stack>
</Authorize>
<SilenceList
items={itemsNotExpired}
alertManagerSourceName={alertManagerSourceName}
dataTestId="not-expired-table"
/>
{itemsExpired.length > 0 && (
<CollapsableSection label={`Expired silences (${itemsExpired.length})`} isOpen={showExpiredFromUrl}>
<div className={styles.callout}>
<Icon className={styles.calloutIcon} name="info-circle" />
<span>
<Trans i18nKey="silences.table.expired-silences">
Expired silences are automatically deleted after 5 days.
</Trans>
</span>
</div>
<SilenceList
items={itemsExpired}
alertManagerSourceName={alertManagerSourceName}
dataTestId="expired-table"
/>
</CollapsableSection>
)}
</Stack>
)}
{!silences.length && <NoSilencesSplash alertManagerSourceName={alertManagerSourceName} />}
</div>
);
};
function SilenceList({
items,
alertManagerSourceName,
dataTestId,
}: {
items: SilenceTableItemProps[];
alertManagerSourceName: string;
dataTestId: string;
}) {
const columns = useColumns(alertManagerSourceName);
if (!!items.length) {
return (
<DynamicTable
pagination={{ itemsPerPage: 25 }}
items={items}
cols={columns}
isExpandable
dataTestId={dataTestId}
renderExpandedContent={({ data }) => {
return (
<>
<Divider />
<SilenceDetails silence={data} />
</>
);
}}
/>
);
} else {
return <Trans i18nKey="silences.table.no-matching-silences">No matching silences found;</Trans>;
}
}
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 = parseMatchers(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 <SilenceStateTag state={status.state} />;
},
size: 3,
},
{
id: 'alert-rule',
label: 'Alert rule targeted',
renderCell: function renderAlertRuleLink({ data: { metadata } }) {
return metadata?.rule_title ? (
<Link
href={`/alerting/grafana/${metadata?.rule_uid}/view?returnTo=${encodeURIComponent('/alerting/silences')}`}
>
{metadata.rule_title}
</Link>
) : (
'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 <Matchers matchers={filteredMatchers} />;
},
size: 7,
},
{
id: 'alerts',
label: 'Alerts silenced',
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
return <span data-testid="alerts">{silencedAlerts.length}</span>;
},
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 (
<Stack gap={0.5} wrap="wrap">
{canRecreate && (
<LinkButton
title="Recreate"
size="sm"
variant="secondary"
icon="sync"
href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
>
<Trans i18nKey="silences.table.recreate-button">Recreate</Trans>
</LinkButton>
)}
{canEdit && (
<>
<LinkButton
title="Unsilence"
size="sm"
variant="secondary"
icon="bell"
onClick={() => handleExpireSilenceClick(silence.id)}
>
<Trans i18nKey="silences.table.unsilence-button">Unsilence</Trans>
</LinkButton>
<LinkButton
title="Edit"
size="sm"
variant="secondary"
icon="pen"
href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
>
<Trans i18nKey="silences.table.edit-button">Edit</Trans>
</LinkButton>
</>
)}
</Stack>
);
},
size: 5,
});
}
return columns;
}, [alertManagerSourceName, expireSilence, isGrafanaFlavoredAlertmanager, updateAllowed, updateSupported]);
}
export default SilencesTable;