From a1d2a9670bc3519a4a7984bc209ec4c3296729c9 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 25 Apr 2024 16:36:33 +0100 Subject: [PATCH] Restructure Silences components for better separation of concerns --- .betterer.results | 7 +- .../features/alerting/unified/Silences.tsx | 104 +++------------ .../components/silences/SilencesEditor.tsx | 61 +++++---- .../components/silences/SilencesTable.tsx | 122 ++++++++++++------ 4 files changed, 139 insertions(+), 155 deletions(-) diff --git a/.betterer.results b/.betterer.results index a0368a6d074..66562bf277d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2159,11 +2159,8 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "4"] ], "public/app/features/alerting/unified/components/silences/SilencesTable.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"], - [0, 0, 0, "Styles should be written using objects.", "4"] + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index ec4addf133e..0663017b29e 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,59 +1,18 @@ -import React, { useCallback, useEffect } from 'react'; +import React from 'react'; import { Route, RouteChildrenProps, Switch } from 'react-router-dom'; -import { Alert, withErrorBoundary } from '@grafana/ui'; -import { Silence } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; +import { withErrorBoundary } from '@grafana/ui'; -import { featureDiscoveryApi } from './api/featureDiscoveryApi'; import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning'; import SilencesEditor from './components/silences/SilencesEditor'; import SilencesTable from './components/silences/SilencesTable'; import { useSilenceNavData } from './hooks/useSilenceNavData'; -import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { useAlertmanager } from './state/AlertmanagerContext'; -import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions'; -import { SILENCES_POLL_INTERVAL_MS } from './utils/constants'; -import { AsyncRequestState, initialAsyncRequestState } from './utils/redux'; const Silences = () => { const { selectedAlertmanager } = useAlertmanager(); - const dispatch = useDispatch(); - const silences = useUnifiedAlertingSelector((state) => state.silences); - const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts); - const alertsRequest = selectedAlertmanager - ? alertsRequests[selectedAlertmanager] || initialAsyncRequestState - : undefined; - - const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery( - { amSourceName: selectedAlertmanager ?? '' }, - { skip: !selectedAlertmanager } - ); - - useEffect(() => { - function fetchAll() { - if (selectedAlertmanager) { - dispatch(fetchSilencesAction(selectedAlertmanager)); - dispatch(fetchAmAlertsAction(selectedAlertmanager)); - } - } - fetchAll(); - const interval = setInterval(() => fetchAll, SILENCES_POLL_INTERVAL_MS); - return () => { - clearInterval(interval); - }; - }, [selectedAlertmanager, dispatch]); - - const { result, loading, error }: AsyncRequestState = - (selectedAlertmanager && silences[selectedAlertmanager]) || initialAsyncRequestState; - - const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]); - - const mimirLazyInitError = - error?.message?.includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit; - if (!selectedAlertmanager) { return null; } @@ -62,48 +21,23 @@ const Silences = () => { <> - {mimirLazyInitError && ( - - Create a new contact point to create a configuration using the default values or contact your administrator to - set up the Alertmanager. - - )} - {error && !loading && !mimirLazyInitError && ( - - {error.message || 'Unknown error.'} - - )} - {alertsRequest?.error && !alertsRequest?.loading && !mimirLazyInitError && ( - - {alertsRequest.error?.message || 'Unknown error.'} - - )} - {result && !error && ( - - - - - - - - - {({ match }: RouteChildrenProps<{ id: string }>) => { - return ( - match?.params.id && ( - - ) - ); - }} - - - )} + + + + + + + + + {({ match }: RouteChildrenProps<{ id: string }>) => { + return ( + match?.params.id && ( + + ) + ); + }} + + ); }; diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index ebcefaced79..1381d54e553 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -1,7 +1,8 @@ import { css, cx } from '@emotion/css'; import { isEqual, pickBy } from 'lodash'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { useHistory } from 'react-router'; import { useDebounce } from 'react-use'; import { @@ -15,25 +16,22 @@ import { } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Button, Field, FieldSet, Input, LinkButton, TextArea, useStyles2 } from '@grafana/ui'; -import { useCleanup } from 'app/core/hooks/useCleanup'; +import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi'; +import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource'; import { Matcher, MatcherOperator, Silence, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; import { useURLSearchParams } from '../../hooks/useURLSearchParams'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; -import { createOrUpdateSilenceAction } from '../../state/actions'; import { SilenceFormFields } from '../../types/silence-form'; import { matcherFieldToMatcher, matcherToMatcherField } from '../../utils/alertmanager'; import { parseQueryParamMatchers } from '../../utils/matchers'; import { makeAMLink } from '../../utils/misc'; -import { initialAsyncRequestState } from '../../utils/redux'; import MatchersField from './MatchersField'; import { SilencePeriod } from './SilencePeriod'; import { SilencedInstancesPreview } from './SilencedInstancesPreview'; interface Props { - silence?: Silence; + silenceId?: string; alertManagerSourceName: string; } @@ -97,24 +95,24 @@ const getDefaultFormValues = (searchParams: URLSearchParams, silence?: Silence): } }; -export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { +export const SilencesEditor = ({ silenceId, alertManagerSourceName }: Props) => { + const history = useHistory(); + const [getSilence, { data: silence, isLoading: getSilenceIsLoading }] = + alertSilencesApi.endpoints.getSilence.useLazyQuery(); + const [createSilence, { isLoading }] = alertSilencesApi.endpoints.createSilence.useMutation(); const [urlSearchParams] = useURLSearchParams(); const defaultValues = useMemo(() => getDefaultFormValues(urlSearchParams, silence), [silence, urlSearchParams]); const formAPI = useForm({ defaultValues }); - const dispatch = useDispatch(); + const styles = useStyles2(getStyles); const [matchersForPreview, setMatchersForPreview] = useState( defaultValues.matchers.map(matcherFieldToMatcher) ); - const { loading } = useUnifiedAlertingSelector((state) => state.updateSilence); + const { register, handleSubmit, formState, watch, setValue, clearErrors, reset } = formAPI; - useCleanup((state) => (state.unifiedAlerting.updateSilence = initialAsyncRequestState)); - - const { register, handleSubmit, formState, watch, setValue, clearErrors } = formAPI; - - const onSubmit = (data: SilenceFormFields) => { + const onSubmit = async (data: SilenceFormFields) => { const { id, startsAt, endsAt, comment, createdBy, matchers: matchersFields } = data; const matchers = matchersFields.map(matcherFieldToMatcher); const payload = pickBy( @@ -128,14 +126,11 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { }, (value) => !!value ) as SilenceCreatePayload; - dispatch( - createOrUpdateSilenceAction({ - alertManagerSourceName, - payload, - exitOnSave: true, - successMessage: `Silence ${payload.id ? 'updated' : 'created'}`, - }) - ); + await createSilence({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName), payload }) + .unwrap() + .then(() => { + history.push(makeAMLink('/alerting/silences', alertManagerSourceName)); + }); }; const duration = watch('duration'); @@ -143,6 +138,16 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { const endsAt = watch('endsAt'); const matcherFields = watch('matchers'); + useEffect(() => { + reset(getDefaultFormValues(urlSearchParams, silence)); + }, [reset, silence, urlSearchParams]); + + useEffect(() => { + if (silenceId) { + getSilence({ id: silenceId, datasourceUid: getDatasourceAPIUid(alertManagerSourceName) }); + } + }, [alertManagerSourceName, getSilence, silenceId]); + // Keep duration and endsAt in sync const [prevDuration, setPrevDuration] = useState(duration); useDebounce( @@ -183,10 +188,14 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => { const userLogged = Boolean(config.bootData.user.isSignedIn && config.bootData.user.name); + if (getSilenceIsLoading) { + return null; + } + return (
-
+
{
- {loading && ( + {isLoading && ( )} - {!loading && } + {!isLoading && } Cancel diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 3d8c21c4b83..5ebd45fc748 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -1,14 +1,17 @@ import { css } from '@emotion/css'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { dateMath, GrafanaTheme2 } from '@grafana/data'; -import { CollapsableSection, Icon, Link, LinkButton, useStyles2, Stack } from '@grafana/ui'; +import { CollapsableSection, Icon, Link, LinkButton, useStyles2, Stack, Alert } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; +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 { SILENCES_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; +import { getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource'; import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types'; -import { useDispatch } from 'app/types'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; -import { expireSilenceAction } from '../../state/actions'; import { parseMatchers } from '../../utils/alertmanager'; import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc'; import { Authorize } from '../Authorize'; @@ -29,12 +32,30 @@ export interface SilenceTableItem extends Silence { type SilenceTableColumnProps = DynamicTableColumnProps; type SilenceTableItemProps = DynamicTableItemProps; interface Props { - silences: Silence[]; - alertManagerAlerts: AlertmanagerAlert[]; alertManagerSourceName: string; } -const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }: Props) => { +const SilencesTable = ({ alertManagerSourceName }: Props) => { + const [getAmAlerts, { data: alertManagerAlerts, isLoading: amAlertsIsLoading }] = + alertmanagerApi.endpoints.getAlertmanagerAlerts.useLazyQuery({ pollingInterval: SILENCES_POLL_INTERVAL_MS }); + const [getSilences, { data: silences = [], isLoading, error }] = alertSilencesApi.endpoints.getSilences.useLazyQuery({ + pollingInterval: SILENCES_POLL_INTERVAL_MS, + }); + + const { currentData: amFeatures } = featureDiscoveryApi.useDiscoverAmFeaturesQuery( + { amSourceName: alertManagerSourceName ?? '' }, + { skip: !alertManagerSourceName } + ); + + const mimirLazyInitError = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any)?.message?.includes('the Alertmanager is not configured') && amFeatures?.lazyConfigInit; + + useEffect(() => { + getSilences({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName) }); + getAmAlerts({ amSourceName: alertManagerSourceName }); + }, [alertManagerSourceName, getAmAlerts, getSilences]); + const styles = useStyles2(getStyles); const [queryParams] = useQueryParams(); const filteredSilencesNotExpired = useFilteredSilences(silences, false); @@ -45,7 +66,7 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }: const itemsNotExpired = useMemo((): SilenceTableItemProps[] => { const findSilencedAlerts = (id: string) => { - return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id)); + return (alertManagerAlerts || []).filter((alert) => alert.status.silencedBy.includes(id)); }; return filteredSilencesNotExpired.map((silence) => { const silencedAlerts = findSilencedAlerts(silence.id); @@ -58,7 +79,7 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }: const itemsExpired = useMemo((): SilenceTableItemProps[] => { const findSilencedAlerts = (id: string) => { - return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id)); + return (alertManagerAlerts || []).filter((alert) => alert.status.silencedBy.includes(id)); }; return filteredSilencesExpired.map((silence) => { const silencedAlerts = findSilencedAlerts(silence.id); @@ -69,6 +90,29 @@ const SilencesTable = ({ silences, alertManagerAlerts, alertManagerSourceName }: }); }, [filteredSilencesExpired, alertManagerAlerts]); + if (isLoading || amAlertsIsLoading) { + return null; + } + + 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) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errMessage = (error as any)?.message || 'Unknown error.'; + return ( + + {errMessage} + + ); + } + return (
{!!silences.length && ( @@ -169,43 +213,43 @@ const useFilteredSilences = (silences: Silence[], expired = false) => { }; 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: ${theme.shape.radius.default}; - height: 62px; - display: flex; - flex-direction: row; - align-items: center; + topButtonContainer: css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + }), + addNewSilence: css({ + margin: theme.spacing(2, 0), + }), + 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', - & > * { - margin-left: ${theme.spacing(1)}; - } - `, - calloutIcon: css` - color: ${theme.colors.info.text}; - `, - editButton: css` - margin-left: ${theme.spacing(0.5)}; - `, + '& > *': { + marginLeft: theme.spacing(1), + }, + }), + calloutIcon: css({ + color: theme.colors.info.text, + }), + editButton: css({ + marginLeft: theme.spacing(0.5), + }), }); function useColumns(alertManagerSourceName: string) { - const dispatch = useDispatch(); const styles = useStyles2(getStyles); const [updateSupported, updateAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateSilence); + const [expireSilence] = alertSilencesApi.endpoints.expireSilence.useMutation(); return useMemo((): SilenceTableColumnProps[] => { - const handleExpireSilenceClick = (id: string) => { - dispatch(expireSilenceAction(alertManagerSourceName, id)); + const handleExpireSilenceClick = (silenceId: string) => { + expireSilence({ datasourceUid: getDatasourceAPIUid(alertManagerSourceName), silenceId }); }; const columns: SilenceTableColumnProps[] = [ { @@ -281,6 +325,6 @@ function useColumns(alertManagerSourceName: string) { }); } return columns; - }, [alertManagerSourceName, dispatch, styles.editButton, updateAllowed, updateSupported]); + }, [alertManagerSourceName, expireSilence, styles.editButton, updateAllowed, updateSupported]); } export default SilencesTable;