mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Restructure Silences components for better separation of concerns
This commit is contained in:
parent
11ed882c84
commit
a1d2a9670b
@ -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"]
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user