Restructure Silences components for better separation of concerns

This commit is contained in:
Tom Ratcliffe 2024-04-25 16:36:33 +01:00 committed by Tom Ratcliffe
parent 11ed882c84
commit a1d2a9670b
4 changed files with 139 additions and 155 deletions

View File

@ -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"]

View File

@ -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<Silence[]> =
(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 = () => {
<>
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager} />
{mimirLazyInitError && (
<Alert title="The selected Alertmanager has no configuration" severity="warning">
Create a new contact point to create a configuration using the default values or contact your administrator to
set up the Alertmanager.
</Alert>
)}
{error && !loading && !mimirLazyInitError && (
<Alert severity="error" title="Error loading silences">
{error.message || 'Unknown error.'}
</Alert>
)}
{alertsRequest?.error && !alertsRequest?.loading && !mimirLazyInitError && (
<Alert severity="error" title="Error loading Alertmanager alerts">
{alertsRequest.error?.message || 'Unknown error.'}
</Alert>
)}
{result && !error && (
<Switch>
<Route exact path="/alerting/silences">
<SilencesTable
silences={result}
alertManagerAlerts={alertsRequest?.result ?? []}
alertManagerSourceName={selectedAlertmanager}
/>
</Route>
<Route exact path="/alerting/silence/new">
<SilencesEditor alertManagerSourceName={selectedAlertmanager} />
</Route>
<Route exact path="/alerting/silence/:id/edit">
{({ match }: RouteChildrenProps<{ id: string }>) => {
return (
match?.params.id && (
<SilencesEditor
silence={getSilenceById(match.params.id)}
alertManagerSourceName={selectedAlertmanager}
/>
)
);
}}
</Route>
</Switch>
)}
<Switch>
<Route exact path="/alerting/silences">
<SilencesTable alertManagerSourceName={selectedAlertmanager} />
</Route>
<Route exact path="/alerting/silence/new">
<SilencesEditor alertManagerSourceName={selectedAlertmanager} />
</Route>
<Route exact path="/alerting/silence/:id/edit">
{({ match }: RouteChildrenProps<{ id: string }>) => {
return (
match?.params.id && (
<SilencesEditor silenceId={match.params.id} alertManagerSourceName={selectedAlertmanager} />
)
);
}}
</Route>
</Switch>
</>
);
};

View File

@ -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<Matcher[]>(
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 (
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit(onSubmit)}>
<FieldSet label={`${silence ? 'Recreate silence' : 'Create silence'}`}>
<FieldSet label={`${silenceId ? 'Recreate silence' : 'Create silence'}`}>
<div className={cx(styles.flexRow, styles.silencePeriod)}>
<SilencePeriod />
<Field
@ -241,12 +250,12 @@ export const SilencesEditor = ({ silence, alertManagerSourceName }: Props) => {
<SilencedInstancesPreview amSourceName={alertManagerSourceName} matchers={matchersForPreview} />
</FieldSet>
<div className={styles.flexRow}>
{loading && (
{isLoading && (
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}
{!loading && <Button type="submit">Save silence</Button>}
{!isLoading && <Button type="submit">Save silence</Button>}
<LinkButton href={makeAMLink('alerting/silences', alertManagerSourceName)} variant={'secondary'}>
Cancel
</LinkButton>

View File

@ -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<SilenceTableItem>;
type SilenceTableItemProps = DynamicTableItemProps<SilenceTableItem>;
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 (
<Alert title="The selected Alertmanager has no configuration" severity="warning">
Create a new contact point to create a configuration using the default values or contact your administrator to
set up the Alertmanager.
</Alert>
);
}
if (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errMessage = (error as any)?.message || 'Unknown error.';
return (
<Alert severity="error" title="Error loading silences">
{errMessage}
</Alert>
);
}
return (
<div data-testid="silences-table">
{!!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;