Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ResponseOps][Alerts] Move the alerts table to a dedicated package #207878

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ packages/kbn-validate-next-docs-cli @elastic/kibana-operations
packages/kbn-web-worker-stub @elastic/kibana-operations
packages/kbn-whereis-pkg-cli @elastic/kibana-operations
packages/kbn-yarn-lock-validator @elastic/kibana-operations
packages/response-ops/alerts_apis @elastic/response-ops
packages/response-ops/alerts_fields_browser @elastic/response-ops
packages/response-ops/alerts_table @elastic/response-ops
packages/serverless/storybook/config @elastic/appex-sharedux
src/core @elastic/kibana-core
src/core/packages/analytics/browser @elastic/kibana-core
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"@kbn/resizable-layout": "link:src/platform/packages/shared/kbn-resizable-layout",
"@kbn/resizable-layout-examples-plugin": "link:examples/resizable_layout_examples",
"@kbn/resolver-test-plugin": "link:x-pack/test/plugin_functional/plugins/resolver_test",
"@kbn/response-ops-alerts-apis": "link:packages/response-ops/alerts_apis",
"@kbn/response-ops-alerts-fields-browser": "link:packages/response-ops/alerts_fields_browser",
"@kbn/response-ops-alerts-table": "link:packages/response-ops/alerts_table",
"@kbn/response-ops-rule-form": "link:src/platform/packages/shared/response-ops/rule_form",
"@kbn/response-ops-rule-params": "link:src/platform/packages/shared/response-ops/rule_params",
"@kbn/response-stream-plugin": "link:examples/response_stream",
Expand Down
3 changes: 3 additions & 0 deletions packages/response-ops/alerts_apis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/response-ops-alerts-apis

Client-side Alerts HTTP API fetchers and React Query wrappers.
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { httpServiceMock } from '@kbn/core/public/mocks';
import { getMutedAlerts } from './get_rules_muted_alerts';
import { getMutedAlertsInstancesByRule } from './get_muted_alerts_instances_by_rule';

const http = httpServiceMock.createStartContract();

describe('getMutedAlerts', () => {
describe('getMutedAlertsInstancesByRule', () => {
const apiRes = {
page: 1,
per_page: 10,
Expand All @@ -24,7 +27,7 @@ describe('getMutedAlerts', () => {
});

test('should call find API with correct params', async () => {
const result = await getMutedAlerts(http, { ruleIds: ['foo'] });
const result = await getMutedAlertsInstancesByRule({ http, ruleIds: ['foo'] });

expect(result).toEqual({
page: 1,
Expand All @@ -45,7 +48,7 @@ describe('getMutedAlerts', () => {
});

test('should call find API with multiple ruleIds', async () => {
const result = await getMutedAlerts(http, { ruleIds: ['foo', 'bar'] });
const result = await getMutedAlertsInstancesByRule({ http, ruleIds: ['foo', 'bar'] });

expect(result).toEqual({
page: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { HttpStart } from '@kbn/core-http-browser';
import { nodeBuilder } from '@kbn/es-query';

const INTERNAL_FIND_RULES_URL = '/internal/alerting/rules/_find';

export interface Rule {
id: string;
muted_alert_ids: string[];
}

export interface FindRulesResponse {
data: Rule[];
}

export interface GetMutedAlertsInstancesByRuleParams {
ruleIds: string[];
http: HttpStart;
signal?: AbortSignal;
}

export const getMutedAlertsInstancesByRule = async ({
http,
ruleIds,
signal,
}: GetMutedAlertsInstancesByRuleParams) => {
const filterNode = nodeBuilder.or(ruleIds.map((id) => nodeBuilder.is('alert.id', `alert:${id}`)));
return http.post<FindRulesResponse>(INTERNAL_FIND_RULES_URL, {
body: JSON.stringify({
filter: JSON.stringify(filterNode),
fields: ['id', 'mutedInstanceIds'],
page: 1,
per_page: ruleIds.length,
}),
signal,
});
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { httpServiceMock } from '@kbn/core/public/mocks';
import { muteAlertInstance } from './mute_alert';
import { muteAlertInstance } from './mute_alert_instance';

const http = httpServiceMock.createStartContract();

Expand Down
25 changes: 25 additions & 0 deletions packages/response-ops/alerts_apis/apis/mute_alert_instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { HttpSetup } from '@kbn/core/public';
import { BASE_ALERTING_API_PATH } from '../constants';

export interface MuteAlertInstanceParams {
id: string;
instanceId: string;
http: HttpSetup;
}

export const muteAlertInstance = ({ id, instanceId, http }: MuteAlertInstanceParams) => {
return http.post<void>(
`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/alert/${encodeURIComponent(
instanceId
)}/_mute`
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { httpServiceMock } from '@kbn/core/public/mocks';
import { unmuteAlertInstance } from './unmute_alert';
import { unmuteAlertInstance } from './unmute_alert_instance';

const http = httpServiceMock.createStartContract();

Expand Down
25 changes: 25 additions & 0 deletions packages/response-ops/alerts_apis/apis/unmute_alert_instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { HttpSetup } from '@kbn/core/public';
import { BASE_ALERTING_API_PATH } from '../constants';

export interface UnmuteAlertInstanceParams {
id: string;
instanceId: string;
http: HttpSetup;
}

export const unmuteAlertInstance = ({ id, instanceId, http }: UnmuteAlertInstanceParams) => {
return http.post<void>(
`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/alert/${encodeURIComponent(
instanceId
)}/_unmute`
);
};
22 changes: 22 additions & 0 deletions packages/response-ops/alerts_apis/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export const BASE_ALERTING_API_PATH = '/api/alerting';

export const queryKeys = {
root: 'alerts',
mutedAlerts: (ruleIds: string[]) =>
[queryKeys.root, 'mutedInstanceIdsForRuleIds', ruleIds] as const,
};

export const mutationKeys = {
root: 'alerts',
muteAlertInstance: () => [mutationKeys.root, 'muteAlertInstance'] as const,
unmuteAlertInstance: () => [mutationKeys.root, 'unmuteAlertInstance'] as const,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { renderHook, waitFor } from '@testing-library/react';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { Wrapper } from '@kbn/alerts-ui-shared/src/common/test_utils/wrapper';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import * as api from '../apis/get_muted_alerts_instances_by_rule';
import { useGetMutedAlertsQuery } from './use_get_muted_alerts_query';

jest.mock('../apis/get_muted_alerts_instances_by_rule');

const ruleIds = ['a', 'b'];

describe('useGetMutedAlertsQuery', () => {
const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
const addErrorMock = notifications.toasts.addError;

beforeEach(() => {
jest.clearAllMocks();
});

it('calls the api when invoked with the correct parameters', async () => {
const muteAlertInstanceSpy = jest.spyOn(api, 'getMutedAlertsInstancesByRule');

renderHook(() => useGetMutedAlertsQuery({ http, notifications, ruleIds }), {
wrapper: Wrapper,
});

await waitFor(() =>
expect(muteAlertInstanceSpy).toHaveBeenCalledWith(expect.objectContaining({ ruleIds }))
);
});

it('does not call the api if the enabled option is false', async () => {
const spy = jest.spyOn(api, 'getMutedAlertsInstancesByRule');

renderHook(() => useGetMutedAlertsQuery({ http, notifications, ruleIds }, { enabled: false }), {
wrapper: Wrapper,
});

await waitFor(() => expect(spy).not.toHaveBeenCalled());
});

it('shows a toast error when the api returns an error', async () => {
const spy = jest
.spyOn(api, 'getMutedAlertsInstancesByRule')
.mockRejectedValue(new Error('An error'));

renderHook(() => useGetMutedAlertsQuery({ http, notifications, ruleIds }), {
wrapper: Wrapper,
});

await waitFor(() => expect(spy).toHaveBeenCalled());
await waitFor(() => expect(addErrorMock).toHaveBeenCalled());
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { i18n } from '@kbn/i18n';
import { useQuery } from '@tanstack/react-query';
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
import { QueryOptionsOverrides } from '@kbn/alerts-ui-shared/src/common/types/tanstack_query_utility_types';
import type { HttpStart } from '@kbn/core-http-browser';
import type { NotificationsStart } from '@kbn/core-notifications-browser';
import { queryKeys } from '../constants';
import { MutedAlerts, ServerError } from '../types';
import {
getMutedAlertsInstancesByRule,
GetMutedAlertsInstancesByRuleParams,
} from '../apis/get_muted_alerts_instances_by_rule';

const ERROR_TITLE = i18n.translate('xpack.responseOpsAlertsApis.mutedAlerts.api.get', {
defaultMessage: 'Error fetching muted alerts data',
});

const getMutedAlerts = ({ http, signal, ruleIds }: GetMutedAlertsInstancesByRuleParams) =>
getMutedAlertsInstancesByRule({ http, ruleIds, signal }).then(({ data: rules }) =>
rules?.reduce((mutedAlerts, rule) => {
mutedAlerts[rule.id] = rule.muted_alert_ids;
return mutedAlerts;
}, {} as MutedAlerts)
);

export interface UseGetMutedAlertsQueryParams {
ruleIds: string[];
http: HttpStart;
notifications: NotificationsStart;
}

export const useGetMutedAlertsQuery = (
{ ruleIds, http, notifications: { toasts } }: UseGetMutedAlertsQueryParams,
{ enabled }: QueryOptionsOverrides<typeof getMutedAlerts> = {}
) => {
return useQuery({
context: AlertsQueryContext,
queryKey: queryKeys.mutedAlerts(ruleIds),
queryFn: ({ signal }) => getMutedAlerts({ http, signal, ruleIds }),
onError: (error: ServerError) => {
if (error.name !== 'AbortError') {
toasts.addError(error.body?.message ? new Error(error.body.message) : error, {
title: ERROR_TITLE,
});
}
},
enabled: ruleIds.length > 0 && enabled !== false,
});
};
Loading