mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Adds contact point sorting and searching (#77390)
This commit is contained in:
parent
60546a9f5a
commit
be436ec6f6
@ -7,6 +7,7 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions, mockDataSource } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
@ -29,10 +30,16 @@ import setupMimirFlavoredServer, { MIMIR_DATASOURCE_UID } from './__mocks__/mimi
|
||||
*
|
||||
* 3. Write tests for the hooks we call in the "container" components
|
||||
* if those have any logic or data structure transformations in them.
|
||||
*
|
||||
* ⚠️ Always set up the MSW server only once – MWS does not support multiple calls to setupServer(); and causes all sorts of weird issues
|
||||
*/
|
||||
describe('ContactPoints', () => {
|
||||
describe('Grafana managed alertmanager', () => {
|
||||
setupGrafanaManagedServer();
|
||||
const server = setupMswServer();
|
||||
|
||||
describe('contact points', () => {
|
||||
describe('Contact points with Grafana managed alertmanager', () => {
|
||||
beforeEach(() => {
|
||||
setupGrafanaManagedServer(server);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([
|
||||
@ -124,10 +131,35 @@ describe('ContactPoints', () => {
|
||||
const deleteButton = screen.getByRole('menuitem', { name: /delete/i });
|
||||
expect(deleteButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should be able to search', async () => {
|
||||
render(
|
||||
<AlertmanagerProvider accessType={'notification'}>
|
||||
<ContactPoints />
|
||||
</AlertmanagerProvider>,
|
||||
{ wrapper: TestProvider }
|
||||
);
|
||||
|
||||
const searchInput = screen.getByRole('textbox', { name: 'search contact points' });
|
||||
await userEvent.type(searchInput, 'slack');
|
||||
expect(searchInput).toHaveValue('slack');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Slack with multiple channels')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('contact-point')).toHaveLength(1);
|
||||
});
|
||||
|
||||
// ⚠️ for some reason, the query params are preserved for all tests so don't forget to clear the input
|
||||
const clearButton = screen.getByRole('button', { name: 'clear' });
|
||||
await userEvent.click(clearButton);
|
||||
expect(searchInput).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mimir-flavored alertmanager', () => {
|
||||
setupMimirFlavoredServer();
|
||||
describe('Contact points with Mimir-flavored alertmanager', () => {
|
||||
beforeEach(() => {
|
||||
setupMimirFlavoredServer(server);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([
|
||||
@ -145,10 +177,11 @@ describe('ContactPoints', () => {
|
||||
|
||||
it('should show / hide loading states', async () => {
|
||||
render(
|
||||
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={MIMIR_DATASOURCE_UID}>
|
||||
<ContactPoints />
|
||||
</AlertmanagerProvider>,
|
||||
{ wrapper: TestProvider }
|
||||
<TestProvider>
|
||||
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={MIMIR_DATASOURCE_UID}>
|
||||
<ContactPoints />
|
||||
</AlertmanagerProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import { SerializedError } from '@reduxjs/toolkit';
|
||||
import { groupBy, size, upperFirst } from 'lodash';
|
||||
import { groupBy, size, uniq, upperFirst } from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
import React, { Fragment, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -8,13 +9,6 @@ import { useToggle } from 'react-use';
|
||||
|
||||
import { dateTime, GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
Alert,
|
||||
Dropdown,
|
||||
Icon,
|
||||
LoadingPlaceholder,
|
||||
Menu,
|
||||
Tooltip,
|
||||
useStyles2,
|
||||
Text,
|
||||
LinkButton,
|
||||
TabsBar,
|
||||
@ -23,6 +17,13 @@ import {
|
||||
Pagination,
|
||||
Button,
|
||||
Stack,
|
||||
Alert,
|
||||
LoadingPlaceholder,
|
||||
useStyles2,
|
||||
Menu,
|
||||
Dropdown,
|
||||
Tooltip,
|
||||
Icon,
|
||||
} from '@grafana/ui';
|
||||
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
||||
import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts';
|
||||
@ -31,6 +32,7 @@ import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting';
|
||||
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { usePagination } from '../../hooks/usePagination';
|
||||
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
@ -48,6 +50,7 @@ import { UnusedContactPointBadge } from '../receivers/ReceiversTable';
|
||||
import { ReceiverMetadataBadge } from '../receivers/grafanaAppReceivers/ReceiverMetadataBadge';
|
||||
import { ReceiverPluginMetadata } from '../receivers/grafanaAppReceivers/useReceiversMetadata';
|
||||
|
||||
import { ContactPointsFilter } from './ContactPointsFilter';
|
||||
import { useDeleteContactPointModal } from './Modals';
|
||||
import { NotificationTemplates } from './NotificationTemplates';
|
||||
import {
|
||||
@ -82,6 +85,9 @@ const ContactPoints = () => {
|
||||
const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger, updateAlertmanagerState.isLoading);
|
||||
const [ExportDrawer, showExportDrawer] = useExportContactPoint();
|
||||
|
||||
const [searchParams] = useURLSearchParams();
|
||||
const { search } = getContactPointsFilters(searchParams);
|
||||
|
||||
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
|
||||
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
|
||||
|
||||
@ -122,10 +128,8 @@ const ContactPoints = () => {
|
||||
) : (
|
||||
<>
|
||||
{/* TODO we can add some additional info here with a ToggleTip */}
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Text variant="body" color="secondary">
|
||||
Define where notifications are sent, a contact point can contain multiple integrations.
|
||||
</Text>
|
||||
<Stack direction="row" alignItems="end">
|
||||
<ContactPointsFilter />
|
||||
<Spacer />
|
||||
<Stack direction="row" gap={1}>
|
||||
{addContactPointSupported && (
|
||||
@ -152,6 +156,7 @@ const ContactPoints = () => {
|
||||
</Stack>
|
||||
<ContactPointsList
|
||||
contactPoints={contactPoints}
|
||||
search={search}
|
||||
pageSize={DEFAULT_PAGE_SIZE}
|
||||
onDelete={(name) => showDeleteModal(name)}
|
||||
disabled={updateAlertmanagerState.isLoading}
|
||||
@ -189,6 +194,7 @@ const ContactPoints = () => {
|
||||
|
||||
interface ContactPointsListProps {
|
||||
contactPoints: ContactPointWithMetadata[];
|
||||
search?: string;
|
||||
disabled?: boolean;
|
||||
onDelete: (name: string) => void;
|
||||
pageSize?: number;
|
||||
@ -197,20 +203,23 @@ interface ContactPointsListProps {
|
||||
const ContactPointsList = ({
|
||||
contactPoints,
|
||||
disabled = false,
|
||||
search,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
onDelete,
|
||||
}: ContactPointsListProps) => {
|
||||
const { page, pageItems, numberOfPages, onPageChange } = usePagination(contactPoints, 1, pageSize);
|
||||
const searchResults = useContactPointsSearch(contactPoints, search);
|
||||
const { page, pageItems, numberOfPages, onPageChange } = usePagination(searchResults, 1, pageSize);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageItems.map((contactPoint, index) => {
|
||||
const provisioned = isProvisioned(contactPoint);
|
||||
const policies = contactPoint.numberOfPolicies;
|
||||
const key = `${contactPoint.name}-${index}`;
|
||||
|
||||
return (
|
||||
<ContactPoint
|
||||
key={`${contactPoint.name}-${index}`}
|
||||
key={key}
|
||||
name={contactPoint.name}
|
||||
disabled={disabled}
|
||||
onDelete={onDelete}
|
||||
@ -225,6 +234,42 @@ const ContactPointsList = ({
|
||||
);
|
||||
};
|
||||
|
||||
const fuzzyFinder = new uFuzzy({
|
||||
intraMode: 1,
|
||||
intraIns: 1,
|
||||
intraSub: 1,
|
||||
intraDel: 1,
|
||||
intraTrn: 1,
|
||||
});
|
||||
|
||||
// let's search in two different haystacks, the name of the contact point and the type of the receiver(s)
|
||||
function useContactPointsSearch(
|
||||
contactPoints: ContactPointWithMetadata[],
|
||||
search?: string
|
||||
): ContactPointWithMetadata[] {
|
||||
const nameHaystack = useMemo(() => {
|
||||
return contactPoints.map((contactPoint) => contactPoint.name);
|
||||
}, [contactPoints]);
|
||||
|
||||
const typeHaystack = useMemo(() => {
|
||||
return contactPoints.map((contactPoint) =>
|
||||
// we're using the resolved metadata key here instead of the "type" property – ex. we alias "teams" to "microsoft teams"
|
||||
contactPoint.grafana_managed_receiver_configs.map((receiver) => receiver[RECEIVER_META_KEY].name).join(' ')
|
||||
);
|
||||
}, [contactPoints]);
|
||||
|
||||
if (!search) {
|
||||
return contactPoints;
|
||||
}
|
||||
|
||||
const nameHits = fuzzyFinder.filter(nameHaystack, search) ?? [];
|
||||
const typeHits = fuzzyFinder.filter(typeHaystack, search) ?? [];
|
||||
|
||||
const hits = [...nameHits, ...typeHits];
|
||||
|
||||
return uniq(hits).map((id) => contactPoints[id]) ?? [];
|
||||
}
|
||||
|
||||
interface ContactPointProps {
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
@ -571,6 +616,10 @@ const useExportContactPoint = (): ExportProps => {
|
||||
return [drawer, handleOpen];
|
||||
};
|
||||
|
||||
const getContactPointsFilters = (searchParams: URLSearchParams) => ({
|
||||
search: searchParams.get('search') ?? undefined,
|
||||
});
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contactPointWrapper: css({
|
||||
borderRadius: `${theme.shape.radius.default}`,
|
||||
|
@ -0,0 +1,61 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Field, Icon, Input, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useURLSearchParams } from '../../hooks/useURLSearchParams';
|
||||
|
||||
const ContactPointsFilter = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [searchParams, setSearchParams] = useURLSearchParams();
|
||||
|
||||
const defaultValue = searchParams.get('search') ?? '';
|
||||
const [searchValue, setSearchValue] = useState(defaultValue);
|
||||
|
||||
const [_, cancel] = useDebounce(
|
||||
() => {
|
||||
setSearchParams({ search: searchValue }, true);
|
||||
},
|
||||
300,
|
||||
[setSearchParams, searchValue]
|
||||
);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
cancel();
|
||||
setSearchValue('');
|
||||
setSearchParams({ search: '' }, true);
|
||||
}, [cancel, setSearchParams]);
|
||||
|
||||
const hasInput = Boolean(defaultValue);
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="end" gap={0.5}>
|
||||
<Field className={styles.noBottom} label="Search by name or type">
|
||||
<Input
|
||||
aria-label="search contact points"
|
||||
placeholder="Search"
|
||||
width={46}
|
||||
prefix={<Icon name="search" />}
|
||||
onChange={(event) => {
|
||||
setSearchValue(event.currentTarget.value);
|
||||
}}
|
||||
value={searchValue}
|
||||
/>
|
||||
</Field>
|
||||
<Button variant="secondary" icon="times" onClick={() => clear()} disabled={!hasInput} aria-label="clear">
|
||||
Clear
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
noBottom: css({
|
||||
marginBottom: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
export { ContactPointsFilter };
|
@ -1,18 +1,17 @@
|
||||
import { rest } from 'msw';
|
||||
import { SetupServer } from 'msw/lib/node';
|
||||
|
||||
import { AlertmanagerChoice, AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ReceiversStateDTO } from 'app/types';
|
||||
|
||||
import { mockApi, setupMswServer } from '../../../mockApi';
|
||||
import { mockApi } from '../../../mockApi';
|
||||
import { mockAlertmanagerChoiceResponse } from '../../../mocks/alertmanagerApi';
|
||||
import { grafanaNotifiersMock } from '../../../mocks/grafana-notifiers';
|
||||
|
||||
import alertmanagerMock from './alertmanager.config.mock.json';
|
||||
import receiversMock from './receivers.mock.json';
|
||||
|
||||
export default () => {
|
||||
const server = setupMswServer();
|
||||
|
||||
export default (server: SetupServer) => {
|
||||
server.use(
|
||||
// this endpoint is a grafana built-in alertmanager
|
||||
rest.get('/api/alertmanager/grafana/config/api/v1/alerts', (_req, res, ctx) =>
|
||||
|
@ -1,17 +1,14 @@
|
||||
import { rest } from 'msw';
|
||||
import { SetupServer } from 'msw/lib/node';
|
||||
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { setupMswServer } from '../../../mockApi';
|
||||
|
||||
import mimirAlertmanagerMock from './alertmanager.mimir.config.mock.json';
|
||||
|
||||
// this one emulates a mimir server setup
|
||||
export const MIMIR_DATASOURCE_UID = 'mimir';
|
||||
|
||||
export default () => {
|
||||
const server = setupMswServer();
|
||||
|
||||
export default (server: SetupServer) => {
|
||||
server.use(
|
||||
rest.get(`/api/alertmanager/${MIMIR_DATASOURCE_UID}/config/api/v1/alerts`, (_req, res, ctx) =>
|
||||
res(ctx.json<AlertManagerCortexConfig>(mimirAlertmanagerMock))
|
||||
@ -22,4 +19,6 @@ export default () => {
|
||||
// this endpoint will respond if the OnCall plugin is installed
|
||||
rest.get('/api/plugins/grafana-oncall-app/settings', (_req, res, ctx) => res(ctx.status(404)))
|
||||
);
|
||||
|
||||
return server;
|
||||
};
|
||||
|
@ -32,6 +32,34 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "grafana-default-email",
|
||||
"numberOfPolicies": 0,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
{
|
||||
"disableResolveMessage": false,
|
||||
"name": "lotsa-emails",
|
||||
"secureFields": {},
|
||||
"settings": {
|
||||
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
|
||||
"singleEmail": false,
|
||||
},
|
||||
"type": "email",
|
||||
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
|
||||
Symbol(receiver_status): {
|
||||
"lastNotifyAttempt": "",
|
||||
"lastNotifyAttemptDuration": "",
|
||||
"name": "email",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications using Grafana server configured SMTP settings",
|
||||
"name": "Email",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
],
|
||||
"name": "lotsa-emails",
|
||||
"numberOfPolicies": 0,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
{
|
||||
@ -61,34 +89,6 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
"name": "provisioned-contact-point",
|
||||
"numberOfPolicies": 0,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
{
|
||||
"disableResolveMessage": false,
|
||||
"name": "lotsa-emails",
|
||||
"secureFields": {},
|
||||
"settings": {
|
||||
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
|
||||
"singleEmail": false,
|
||||
},
|
||||
"type": "email",
|
||||
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
|
||||
Symbol(receiver_status): {
|
||||
"lastNotifyAttempt": "",
|
||||
"lastNotifyAttemptDuration": "",
|
||||
"name": "email",
|
||||
"sendResolved": true,
|
||||
},
|
||||
Symbol(receiver_metadata): {
|
||||
"description": "Sends notifications using Grafana server configured SMTP settings",
|
||||
"name": "Email",
|
||||
},
|
||||
Symbol(receiver_plugin_metadata): undefined,
|
||||
},
|
||||
],
|
||||
"name": "lotsa-emails",
|
||||
"numberOfPolicies": 0,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
{
|
||||
|
@ -4,14 +4,19 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
|
||||
import setupGrafanaManagedServer from './__mocks__/grafanaManagedServer';
|
||||
import { useContactPointsWithStatus } from './useContactPoints';
|
||||
|
||||
const server = setupMswServer();
|
||||
|
||||
describe('useContactPoints', () => {
|
||||
setupGrafanaManagedServer();
|
||||
beforeEach(() => {
|
||||
setupGrafanaManagedServer(server);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
|
@ -91,7 +91,7 @@ export function useContactPointsWithStatus() {
|
||||
onCallPluginStatusLoading ||
|
||||
onCallPluginIntegrationsLoading;
|
||||
|
||||
const contactPoints = fetchAlertmanagerConfiguration.contactPoints;
|
||||
const contactPoints = fetchAlertmanagerConfiguration.contactPoints.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return {
|
||||
error,
|
||||
|
@ -87,7 +87,15 @@ const rulerRuleIdentifier = ruleId.fromRulerRule('prometheus', 'ns-default', 'gr
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
const promDsSettings = mockDataSource({
|
||||
name: dsName,
|
||||
uid: dsName,
|
||||
});
|
||||
|
||||
setupDataSources(promDsSettings);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// some action buttons need to check what Alertmanager setup we have for Grafana managed rules
|
||||
mockAlertmanagerChoiceResponse(server, {
|
||||
alertmanagersChoice: AlertmanagerChoice.Internal,
|
||||
@ -96,13 +104,6 @@ beforeAll(() => {
|
||||
// we need to mock this one for the "declare incident" button
|
||||
mockPluginSettings(server, SupportedPlugin.Incident);
|
||||
|
||||
const promDsSettings = mockDataSource({
|
||||
name: dsName,
|
||||
uid: dsName,
|
||||
});
|
||||
|
||||
setupDataSources(promDsSettings);
|
||||
|
||||
mockAlertRuleApi(server).rulerRules('grafana', {
|
||||
[mockGrafanaRule.namespace.name]: [
|
||||
{ name: mockGrafanaRule.group.name, interval: '1m', rules: [mockGrafanaRule.rulerRule!] },
|
||||
|
@ -427,6 +427,10 @@ export function setupMswServer() {
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
@ -65,17 +65,19 @@ const fakeResponse: PromRulesResponse = {
|
||||
|
||||
const server = setupMswServer();
|
||||
|
||||
mockPromRulesApiResponse(server, fakeResponse);
|
||||
const originRule: RulerGrafanaRuleDTO = mockRulerGrafanaRule(
|
||||
{
|
||||
for: '1m',
|
||||
labels: { severity: 'critical', region: 'nasa' },
|
||||
annotations: { [Annotation.summary]: 'This is a very important alert rule' },
|
||||
},
|
||||
{ uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] }
|
||||
);
|
||||
mockRulerRulesApiResponse(server, 'grafana', {
|
||||
'folder-one': [{ name: 'group1', interval: '20s', rules: [originRule] }],
|
||||
beforeEach(() => {
|
||||
mockPromRulesApiResponse(server, fakeResponse);
|
||||
const originRule: RulerGrafanaRuleDTO = mockRulerGrafanaRule(
|
||||
{
|
||||
for: '1m',
|
||||
labels: { severity: 'critical', region: 'nasa' },
|
||||
annotations: { [Annotation.summary]: 'This is a very important alert rule' },
|
||||
},
|
||||
{ uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] }
|
||||
);
|
||||
mockRulerRulesApiResponse(server, 'grafana', {
|
||||
'folder-one': [{ name: 'group1', interval: '20s', rules: [originRule] }],
|
||||
});
|
||||
});
|
||||
|
||||
const defaultOptions: UnifiedAlertListOptions = {
|
||||
|
Loading…
Reference in New Issue
Block a user