From 8485deb2c01345bae076c5d97a4f09b059af45da Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Mon, 17 Apr 2023 16:30:41 +0200 Subject: [PATCH] Alerting: Split silences view expired/not-expired (#66562) Co-authored-by: Gilles De Mey --- .../alerting/unified/Silences.test.tsx | 31 +++-- .../unified/components/DynamicTable.tsx | 4 +- .../components/silences/SilencesFilter.tsx | 26 +--- .../components/silences/SilencesTable.tsx | 121 +++++++++++------- 4 files changed, 108 insertions(+), 74 deletions(-) diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 4c333b57825..048aa24c3b9 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -5,13 +5,15 @@ import { TestProvider } from 'test/helpers/TestProvider'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; import { dateTime } from '@grafana/data'; -import { locationService, setDataSourceSrv, config } from '@grafana/runtime'; +import { config, locationService, setDataSourceSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; +import { SilenceState } from '../../../plugins/datasource/alertmanager/types'; + import Silences from './Silences'; -import { fetchSilences, fetchAlerts, createOrUpdateSilence } from './api/alertmanager'; +import { createOrUpdateSilence, fetchAlerts, fetchSilences } from './api/alertmanager'; import { mockAlertmanagerAlert, mockDataSource, MockDataSourceSrv, mockSilence } from './mocks'; import { parseMatchers } from './utils/alertmanager'; import { DataSourceType } from './utils/datasource'; @@ -48,10 +50,12 @@ const dataSources = { }; const ui = { - silencesTable: byTestId('dynamic-table'), + notExpiredTable: byTestId('not-expired-table'), + expiredTable: byTestId('expired-table'), + expiredCaret: byText(/expired/i), silenceRow: byTestId('row'), silencedAlertCell: byTestId('alerts'), - addSilenceButton: byRole('button', { name: /add silence/i }), + addSilenceButton: byRole('link', { name: /add silence/i }), queryBar: byPlaceholderText('Search'), editor: { timeRange: byLabelText('Timepicker', { exact: false }), @@ -75,6 +79,7 @@ const resetMocks = () => { return Promise.resolve([ mockSilence({ id: '12345' }), mockSilence({ id: '67890', matchers: parseMatchers('foo!=bar'), comment: 'Catch all' }), + mockSilence({ id: '1111', status: { state: SilenceState.Expired } }), ]); }); @@ -128,9 +133,19 @@ describe('Silences', () => { await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); - expect(ui.silencesTable.query()).not.toBeNull(); + await userEvent.click(ui.expiredCaret.get()); + expect(ui.notExpiredTable.get()).not.toBeNull(); + expect(ui.expiredTable.get()).not.toBeNull(); + let silences = ui.silenceRow.queryAll(); + expect(silences).toHaveLength(3); + expect(silences[0]).toHaveTextContent('foo=bar'); + expect(silences[1]).toHaveTextContent('foo!=bar'); + expect(silences[2]).toHaveTextContent('foo=bar'); - const silences = ui.silenceRow.queryAll(); + await userEvent.click(ui.expiredCaret.getAll()[0]); + expect(ui.notExpiredTable.get()).not.toBeNull(); + expect(ui.expiredTable.query()).toBeNull(); + silences = ui.silenceRow.queryAll(); expect(silences).toHaveLength(2); expect(silences[0]).toHaveTextContent('foo=bar'); expect(silences[1]).toHaveTextContent('foo!=bar'); @@ -158,7 +173,7 @@ describe('Silences', () => { await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled()); await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled()); - const silencedAlertRows = ui.silencedAlertCell.getAll(ui.silencesTable.get()); + const silencedAlertRows = ui.silencedAlertCell.getAll(ui.notExpiredTable.get()); expect(silencedAlertRows).toHaveLength(2); expect(silencedAlertRows[0]).toHaveTextContent('2'); expect(silencedAlertRows[1]).toHaveTextContent('0'); @@ -177,7 +192,7 @@ describe('Silences', () => { await userEvent.click(queryBar); await userEvent.paste('foo=bar'); - await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(1)); + await waitFor(() => expect(ui.silenceRow.getAll()).toHaveLength(2)); }, TEST_TIMEOUT ); diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index bf925ffc8ce..deae6c1a7eb 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -28,6 +28,7 @@ export interface DynamicTableItemProps { export interface DynamicTableProps { cols: Array>; items: Array>; + dataTestId?: string; isExpandable?: boolean; pagination?: DynamicTablePagination; @@ -70,6 +71,7 @@ export const DynamicTable = ({ renderPrefixCell, renderPrefixHeader, footerRow, + dataTestId, }: DynamicTableProps) => { const defaultPaginationStyles = useStyles2(getPaginationStyles); @@ -98,7 +100,7 @@ export const DynamicTable = ({ return ( <> -
+
{renderPrefixHeader && renderPrefixHeader()} {isExpandable &&
} diff --git a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx index 2d926a9fe41..acb6b61aa66 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx @@ -2,26 +2,20 @@ import { css } from '@emotion/css'; import { debounce, uniqueId } from 'lodash'; import React, { FormEvent, useState } from 'react'; -import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { Label, Icon, Input, Tooltip, RadioButtonGroup, useStyles2, Button, Field } from '@grafana/ui'; +import { Button, Field, Icon, Input, Label, Tooltip, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { SilenceState } from 'app/plugins/datasource/alertmanager/types'; import { parseMatchers } from '../../utils/alertmanager'; import { getSilenceFiltersFromUrlParams } from '../../utils/misc'; -const stateOptions: SelectableValue[] = Object.entries(SilenceState).map(([key, value]) => ({ - label: key, - value, -})); - const getQueryStringKey = () => uniqueId('query-string-'); export const SilencesFilter = () => { const [queryStringKey, setQueryStringKey] = useState(getQueryStringKey()); const [queryParams, setQueryParams] = useQueryParams(); - const { queryString, silenceState } = getSilenceFiltersFromUrlParams(queryParams); + const { queryString } = getSilenceFiltersFromUrlParams(queryParams); const styles = useStyles2(getStyles); const handleQueryStringChange = debounce((e: FormEvent) => { @@ -29,10 +23,6 @@ export const SilencesFilter = () => { setQueryParams({ queryString: target.value || null }); }, 400); - const handleSilenceStateChange = (state: string) => { - setQueryParams({ silenceState: state }); - }; - const clearFilters = () => { setQueryParams({ queryString: null, @@ -77,10 +67,8 @@ export const SilencesFilter = () => { data-testid="search-query-input" /> - - - - {(queryString || silenceState) && ( + + {queryString && (
- + + Add Silence +
- {!!items.length ? ( - <> - } + + {itemsExpired.length > 0 && ( + +
+ + Expired silences are automatically deleted after 5 days. +
+ - {showExpiredSilencesBanner && ( -
- - Expired silences are automatically deleted after 5 days. -
- )} - - ) : ( - 'No matching silences found' +
)} - + )} {!silences.length && }
); }; -const useFilteredSilences = (silences: Silence[]) => { +function SilenceList({ + items, + alertManagerSourceName, + dataTestId, +}: { + items: SilenceTableItemProps[]; + alertManagerSourceName: string; + dataTestId: string; +}) { + const columns = useColumns(alertManagerSourceName); + if (!!items.length) { + return ( + } + /> + ); + } else { + return <>No matching silences found; + } +} + +const useFilteredSilences = (silences: Silence[], expired = false) => { const [queryParams] = useQueryParams(); return useMemo(() => { - const { queryString, silenceState } = getSilenceFiltersFromUrlParams(queryParams); + const { queryString } = getSilenceFiltersFromUrlParams(queryParams); const silenceIdsString = queryParams?.silenceIds; return silences.filter((silence) => { if (typeof silenceIdsString === 'string') { @@ -128,15 +161,13 @@ const useFilteredSilences = (silences: Silence[]) => { return false; } } - if (silenceState) { - const stateMatches = silence.status.state === silenceState; - if (!stateMatches) { - return false; - } + if (expired) { + return silence.status.state === SilenceState.Expired; + } else { + return silence.status.state !== SilenceState.Expired; } - return true; }); - }, [queryParams, silences]); + }, [queryParams, silences, expired]); }; const getStyles = (theme: GrafanaTheme2) => ({ @@ -156,7 +187,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: flex; flex-direction: row; align-items: center; - margin-top: ${theme.spacing(2)}; & > * { margin-left: ${theme.spacing(1)}; @@ -255,5 +285,4 @@ function useColumns(alertManagerSourceName: string) { return columns; }, [alertManagerSourceName, dispatch, styles, permissions]); } - export default SilencesTable;