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

253 lines
8.3 KiB

import React, { FC, useMemo } from 'react';
import { GrafanaTheme2, dateMath } from '@grafana/data';
import { Icon, useStyles2, Link, Button } from '@grafana/ui';
import { css } from '@emotion/css';
import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';
import { NoSilencesSplash } from './NoSilencesCTA';
import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
import { contextSrv } from 'app/core/services/context_srv';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { SilencesFilter } from './SilencesFilter';
import { parseMatchers } from '../../utils/alertmanager';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { SilenceStateTag } from './SilenceStateTag';
import { Matchers } from './Matchers';
import { ActionButton } from '../rules/ActionButton';
import { ActionIcon } from '../rules/ActionIcon';
import { useDispatch } from 'react-redux';
import { expireSilenceAction } from '../../state/actions';
import { SilenceDetails } from './SilenceDetails';
import { Stack } from '@grafana/experimental';
export interface SilenceTableItem extends Silence {
silencedAlerts: AlertmanagerAlert[];
}
type SilenceTableColumnProps = DynamicTableColumnProps<SilenceTableItem>;
type SilenceTableItemProps = DynamicTableItemProps<SilenceTableItem>;
interface Props {
silences: Silence[];
alertManagerAlerts: AlertmanagerAlert[];
alertManagerSourceName: string;
}
const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSourceName }) => {
const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams();
const filteredSilences = useFilteredSilences(silences);
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
const showExpiredSilencesBanner =
!!filteredSilences.length && (silenceState === undefined || silenceState === SilenceState.Expired);
const columns = useColumns(alertManagerSourceName);
const items = useMemo((): SilenceTableItemProps[] => {
const findSilencedAlerts = (id: string) => {
return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id));
};
return filteredSilences.map((silence) => {
const silencedAlerts = findSilencedAlerts(silence.id);
return {
id: silence.id,
data: { ...silence, silencedAlerts },
};
});
}, [filteredSilences, alertManagerAlerts]);
return (
<div data-testid="silences-table">
{!!silences.length && (
<>
<SilencesFilter />
{contextSrv.isEditor && (
<div className={styles.topButtonContainer}>
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
<Button className={styles.addNewSilence} icon="plus">
New Silence
</Button>
</Link>
</div>
)}
{!!items.length ? (
<>
<DynamicTable
items={items}
cols={columns}
isExpandable
renderExpandedContent={({ data }) => <SilenceDetails silence={data} />}
/>
{showExpiredSilencesBanner && (
<div className={styles.callout}>
<Icon className={styles.calloutIcon} name="info-circle" />
<span>Expired silences are automatically deleted after 5 days.</span>
</div>
)}
</>
) : (
'No matching silences found'
)}
</>
)}
{!silences.length && <NoSilencesSplash alertManagerSourceName={alertManagerSourceName} />}
</div>
);
};
const useFilteredSilences = (silences: Silence[]) => {
const [queryParams] = useQueryParams();
return useMemo(() => {
const { queryString, silenceState } = 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 (silenceState) {
const stateMatches = silence.status.state === silenceState;
if (!stateMatches) {
return false;
}
}
return true;
});
}, [queryParams, silences]);
};
const getStyles = (theme: GrafanaTheme2) => ({
topButtonContainer: css`
display: flex;
flex-direction: row;
justify-content: flex-end;
`,
addNewSilence: css`
margin: ${theme.spacing(2, 0)};
`,
callout: css`
background-color: ${theme.colors.background.secondary};
border-top: 3px solid ${theme.colors.info.border};
border-radius: 2px;
height: 62px;
display: flex;
flex-direction: row;
align-items: center;
margin-top: ${theme.spacing(2)};
& > * {
margin-left: ${theme.spacing(1)};
}
`,
calloutIcon: css`
color: ${theme.colors.info.text};
`,
editButton: css`
margin-left: ${theme.spacing(0.5)};
`,
});
function useColumns(alertManagerSourceName: string) {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
return useMemo((): SilenceTableColumnProps[] => {
const handleExpireSilenceClick = (id: string) => {
dispatch(expireSilenceAction(alertManagerSourceName, id));
};
const showActions = contextSrv.isEditor;
const columns: SilenceTableColumnProps[] = [
{
id: 'state',
label: 'State',
renderCell: function renderStateTag({ data: { status } }) {
return <SilenceStateTag state={status.state} />;
},
size: '88px',
},
{
id: 'matchers',
label: 'Matching labels',
renderCell: function renderMatchers({ data: { matchers } }) {
return <Matchers matchers={matchers || []} />;
},
size: 9,
},
{
id: 'alerts',
label: 'Alerts',
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
return <span data-testid="alerts">{silencedAlerts.length}</span>;
},
size: 1,
},
{
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)} {'-'}
<br />
{endsAtDate?.format(dateDisplayFormat)}
</>
);
},
size: '150px',
},
];
if (showActions) {
columns.push({
id: 'actions',
label: 'Actions',
renderCell: function renderActions({ data: silence }) {
return (
<Stack gap={0.5}>
{silence.status.state === 'expired' ? (
<Link href={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}>
<ActionButton icon="sync">Recreate</ActionButton>
</Link>
) : (
<ActionButton icon="bell" onClick={() => handleExpireSilenceClick(silence.id)}>
Unsilence
</ActionButton>
)}
{silence.status.state !== 'expired' && (
<ActionIcon
className={styles.editButton}
to={makeAMLink(`/alerting/silence/${silence.id}/edit`, alertManagerSourceName)}
icon="pen"
tooltip="edit"
/>
)}
</Stack>
);
},
size: '147px',
});
}
return columns;
}, [alertManagerSourceName, dispatch, styles]);
}
export default SilencesTable;