Skip to content

Commit

Permalink
[ResponseOps][Alerts] Move the alerts table to a dedicated package (e…
Browse files Browse the repository at this point in the history
…lastic#207878)

## Summary

This PR turns the AlertsTable into a standalone component, making it
independent from the `TriggersActionsUI` plugin.

#### Removes the alerts table registry

All configuration is now managed through the AlertsTable component
props. Shared configurations are handled by giving consumers the ability
to directly provide alerts table wrapper components (see for example the
`renderAlertsTable` prop of `getCases`).

#### Moves the alerts table to dedicated package(s)

Following the feature-driven structure we're introducing for ResponseOps
(alerting) client-side packages:
- `@kbn/response-ops-alerts-table`
- `@kbn/response-ops-alerts-apis`
- `@kbn/response-ops-alerts-fields-browser`

#### Initial work on improving composition and organization

- Reorganizes the table code into a by-entity-type folder structure
(`components/`, `hooks/`, ...)
- Simplifies some components and breaks into smaller units when possible

## To verify

For consumers of the alerts table:
- Check that all your tables have the same behavior as before (columns,
sort, row actions, bulk actions, etc.)
- Check that your "shared" tables (i.e. cases alerts view in O11y and
Security) have the expected configuration and behavior

> [!WARNING]
> This PR moves a lot of files. Git might not always recognize the
correct delete/add file pairs. If you see weird diffs feel free to reach
out for help!

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

### Identify risks

| Risk | Description | Severity | Mitigation |
|---|---|---|---|
| Table misconfigurations | Some table configurations might slightly
differ from the previous AlertsTableRegistry-backed version | Low |
Quick fix |

## References

Closes elastic#195180

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Christos Nasikas <xristosnasikas@gmail.com>
(cherry picked from commit a74066d)

# Conflicts:
#	x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/index.tsx
#	x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.tsx
#	x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_profile_renderer.tsx
  • Loading branch information
umbopepato committed Feb 12, 2025
1 parent bc9e1a5 commit ec1aa8b
Show file tree
Hide file tree
Showing 436 changed files with 11,991 additions and 11,923 deletions.
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

0 comments on commit ec1aa8b

Please sign in to comment.