mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ResponseOps][Alerts] Move the alerts table to a dedicated package (#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 #195180 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas <xristosnasikas@gmail.com>
This commit is contained in:
parent
d8789819fd
commit
a74066d6f8
423 changed files with 11964 additions and 11917 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
3
packages/response-ops/alerts_apis/README.md
Normal file
3
packages/response-ops/alerts_apis/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/response-ops-alerts-apis
|
||||
|
||||
Client-side Alerts HTTP API fetchers and React Query wrappers.
|
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
|
|
@ -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`
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
|
|
@ -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
packages/response-ops/alerts_apis/constants.ts
Normal file
22
packages/response-ops/alerts_apis/constants.ts
Normal file
|
@ -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,
|
||||
};
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { Wrapper } from '@kbn/alerts-ui-shared/src/common/test_utils/wrapper';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import * as api from '../apis/mute_alert_instance';
|
||||
import { useMuteAlertInstance } from './use_mute_alert_instance';
|
||||
|
||||
jest.mock('../apis/mute_alert_instance');
|
||||
|
||||
const params = { ruleId: '', alertInstanceId: '' };
|
||||
|
||||
describe('useMuteAlertInstance', () => {
|
||||
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, 'muteAlertInstance');
|
||||
|
||||
const { result } = renderHook(() => useMuteAlertInstance({ http, notifications }), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
result.current.mutate(params);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(muteAlertInstanceSpy).toHaveBeenCalledWith({
|
||||
id: params.ruleId,
|
||||
instanceId: params.alertInstanceId,
|
||||
http: expect.anything(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a toast error when the api returns an error', async () => {
|
||||
const spy = jest.spyOn(api, 'muteAlertInstance').mockRejectedValue(new Error('An error'));
|
||||
|
||||
const { result } = renderHook(() => useMuteAlertInstance({ http, notifications }), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
result.current.mutate(params);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(addErrorMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { useMutation } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
import { mutationKeys } from '../constants';
|
||||
import type { ServerError, ToggleAlertParams } from '../types';
|
||||
import { muteAlertInstance } from '../apis/mute_alert_instance';
|
||||
|
||||
const ERROR_TITLE = i18n.translate('alertsApis.muteAlert.error', {
|
||||
defaultMessage: 'Error muting alert',
|
||||
});
|
||||
|
||||
export interface UseMuteAlertInstanceParams {
|
||||
http: HttpStart;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
|
||||
export const useMuteAlertInstance = ({
|
||||
http,
|
||||
notifications: { toasts },
|
||||
}: UseMuteAlertInstanceParams) => {
|
||||
return useMutation(
|
||||
({ ruleId, alertInstanceId }: ToggleAlertParams) =>
|
||||
muteAlertInstance({ http, id: ruleId, instanceId: alertInstanceId }),
|
||||
{
|
||||
mutationKey: mutationKeys.muteAlertInstance(),
|
||||
context: AlertsQueryContext,
|
||||
onSuccess() {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.responseOpsAlertsApis.alertsTable.alertMuted', {
|
||||
defaultMessage: 'Alert muted',
|
||||
})
|
||||
);
|
||||
},
|
||||
onError: (error: ServerError) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts.addError(
|
||||
error.body && error.body.message ? new Error(error.body.message) : error,
|
||||
{
|
||||
title: ERROR_TITLE,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { Wrapper } from '@kbn/alerts-ui-shared/src/common/test_utils/wrapper';
|
||||
import { useUnmuteAlertInstance } from './use_unmute_alert_instance';
|
||||
import * as api from '../apis/unmute_alert_instance';
|
||||
|
||||
jest.mock('../apis/unmute_alert_instance');
|
||||
|
||||
const params = { ruleId: '', alertInstanceId: '' };
|
||||
|
||||
describe('useUnmuteAlertInstance', () => {
|
||||
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, 'unmuteAlertInstance');
|
||||
|
||||
const { result } = renderHook(() => useUnmuteAlertInstance({ http, notifications }), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
result.current.mutate(params);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(muteAlertInstanceSpy).toHaveBeenCalledWith({
|
||||
id: params.ruleId,
|
||||
instanceId: params.alertInstanceId,
|
||||
http: expect.anything(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a toast error when the api returns an error', async () => {
|
||||
const spy = jest.spyOn(api, 'unmuteAlertInstance').mockRejectedValue(new Error('An error'));
|
||||
|
||||
const { result } = renderHook(() => useUnmuteAlertInstance({ http, notifications }), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
result.current.mutate(params);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(addErrorMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { useMutation } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
import { mutationKeys } from '../constants';
|
||||
import type { ServerError, ToggleAlertParams } from '../types';
|
||||
import { unmuteAlertInstance } from '../apis/unmute_alert_instance';
|
||||
|
||||
const ERROR_TITLE = i18n.translate('alertsApis.unmuteAlert.error', {
|
||||
defaultMessage: 'Error unmuting alert',
|
||||
});
|
||||
|
||||
export interface UseUnmuteAlertInstanceParams {
|
||||
http: HttpStart;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
|
||||
export const useUnmuteAlertInstance = ({
|
||||
http,
|
||||
notifications: { toasts },
|
||||
}: UseUnmuteAlertInstanceParams) => {
|
||||
return useMutation(
|
||||
({ ruleId, alertInstanceId }: ToggleAlertParams) =>
|
||||
unmuteAlertInstance({ http, id: ruleId, instanceId: alertInstanceId }),
|
||||
{
|
||||
mutationKey: mutationKeys.unmuteAlertInstance(),
|
||||
context: AlertsQueryContext,
|
||||
onSuccess() {
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.responseOpsAlertsApis.alertsTable.alertUnmuted', {
|
||||
defaultMessage: 'Alert unmuted',
|
||||
})
|
||||
);
|
||||
},
|
||||
onError: (error: ServerError) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
toasts.addError(
|
||||
error.body && error.body.message ? new Error(error.body.message) : error,
|
||||
{
|
||||
title: ERROR_TITLE,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
15
packages/response-ops/alerts_apis/jest.config.js
Normal file
15
packages/response-ops/alerts_apis/jest.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/response-ops/alerts_apis'],
|
||||
setupFilesAfterEnv: ['<rootDir>/packages/response-ops/alerts_apis/setup_tests.ts'],
|
||||
};
|
7
packages/response-ops/alerts_apis/kibana.jsonc
Normal file
7
packages/response-ops/alerts_apis/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/response-ops-alerts-apis",
|
||||
"owner": "@elastic/response-ops",
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
6
packages/response-ops/alerts_apis/package.json
Normal file
6
packages/response-ops/alerts_apis/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/response-ops-alerts-apis",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
11
packages/response-ops/alerts_apis/setup_tests.ts
Normal file
11
packages/response-ops/alerts_apis/setup_tests.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-styled-components';
|
27
packages/response-ops/alerts_apis/tsconfig.json
Normal file
27
packages/response-ops/alerts_apis/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/i18n",
|
||||
"@kbn/alerts-ui-shared",
|
||||
"@kbn/core-notifications-browser",
|
||||
"@kbn/core-http-browser-mocks",
|
||||
"@kbn/core-notifications-browser-mocks",
|
||||
"@kbn/es-query"
|
||||
]
|
||||
}
|
22
packages/response-ops/alerts_apis/types.ts
Normal file
22
packages/response-ops/alerts_apis/types.ts
Normal file
|
@ -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".
|
||||
*/
|
||||
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
|
||||
|
||||
export type ServerError = IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
export interface ToggleAlertParams {
|
||||
ruleId: string;
|
||||
alertInstanceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map from rule ids to muted alert instance ids
|
||||
*/
|
||||
export type MutedAlerts = Record<string, string[]>;
|
3
packages/response-ops/alerts_fields_browser/README.md
Normal file
3
packages/response-ops/alerts_fields_browser/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/response-ops-alerts-fields-browser
|
||||
|
||||
A picker component for alert document fields.
|
|
@ -7,15 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TechnicalRuleDataFieldName } from '@kbn/rule-data-utils';
|
||||
import { css } from '@emotion/react';
|
||||
import { UseEuiTheme } from '@elastic/eui';
|
||||
|
||||
export interface BasicFields {
|
||||
_id: string;
|
||||
_index: string;
|
||||
}
|
||||
|
||||
export type Alert = BasicFields & {
|
||||
[Property in TechnicalRuleDataFieldName]?: string[];
|
||||
} & {
|
||||
[x: string]: unknown[];
|
||||
export const styles = {
|
||||
badgesGroup: ({ euiTheme }: { euiTheme: UseEuiTheme['euiTheme'] }) => css`
|
||||
margin-top: ${euiTheme.size.xs};
|
||||
min-height: 24px;
|
||||
`,
|
||||
};
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React, { useCallback } from 'react';
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
import { styles } from './categories_badges.styles';
|
|
@ -7,11 +7,5 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export interface LegacyField {
|
||||
field: string;
|
||||
value: string[];
|
||||
}
|
||||
export interface EsQuerySnapshot {
|
||||
request: string[];
|
||||
response: string[];
|
||||
}
|
||||
export { CategoriesBadges } from './categories_badges';
|
||||
export type { CategoriesBadgesProps } from './categories_badges';
|
|
@ -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".
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
export const styles = {
|
||||
countBadge: css`
|
||||
margin-left: 5px;
|
||||
`,
|
||||
categoryName: ({ bold }: { bold: boolean }) => css`
|
||||
font-weight: ${bold ? 'bold' : 'normal'};
|
||||
`,
|
||||
selectableContainer: css`
|
||||
width: 300px;
|
||||
`,
|
||||
};
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useMemo, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
|
@ -17,7 +20,7 @@ import {
|
|||
EuiSelectable,
|
||||
FilterChecked,
|
||||
} from '@elastic/eui';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import * as i18n from '../../translations';
|
||||
import { getFieldCount, isEscape } from '../../helpers';
|
||||
import { styles } from './categories_selector.styles';
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { CategoriesSelector } from './categories_selector';
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
|
||||
export const styles = {
|
||||
buttonContainer: css`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
`,
|
||||
};
|
|
@ -1,17 +1,19 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { act, fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
import { mockBrowserFields } from './mock';
|
||||
import { FIELD_BROWSER_WIDTH } from './helpers';
|
||||
import { mockBrowserFields } from '../../mock';
|
||||
import { FIELD_BROWSER_WIDTH } from '../../helpers';
|
||||
import { FieldBrowserComponent } from './field_browser';
|
||||
import type { FieldBrowserProps } from './types';
|
||||
import type { FieldBrowserProps } from '../../types';
|
||||
|
||||
const defaultProps: FieldBrowserProps = {
|
||||
browserFields: mockBrowserFields,
|
|
@ -1,18 +1,21 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { FieldBrowserProps } from './types';
|
||||
import { FieldBrowserModal } from './field_browser_modal';
|
||||
import { filterBrowserFieldsByFieldName, filterSelectedBrowserFields } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { FieldBrowserProps } from '../../types';
|
||||
import { FieldBrowserModal } from '../field_browser_modal/field_browser_modal';
|
||||
import { filterBrowserFieldsByFieldName, filterSelectedBrowserFields } from '../../helpers';
|
||||
import * as i18n from '../../translations';
|
||||
import { styles } from './field_browser.styles';
|
||||
|
||||
const FIELDS_BUTTON_CLASS_NAME = 'fields-button';
|
|
@ -1,14 +1,16 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { mockBrowserFields } from './mock';
|
||||
import { mockBrowserFields } from '../../mock';
|
||||
import { FieldBrowserModal, FieldBrowserModalProps } from './field_browser_modal';
|
||||
|
||||
const mockOnHide = jest.fn();
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -18,16 +21,20 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { FieldBrowserProps } from './types';
|
||||
import { Search } from './components/search';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { FieldBrowserProps } from '../../types';
|
||||
import { Search } from '../search';
|
||||
|
||||
import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers';
|
||||
import {
|
||||
CLOSE_BUTTON_CLASS_NAME,
|
||||
FIELD_BROWSER_WIDTH,
|
||||
RESET_FIELDS_CLASS_NAME,
|
||||
} from '../../helpers';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { CategoriesSelector } from './components/categories_selector';
|
||||
import { CategoriesBadges } from './components/categories_badges';
|
||||
import { FieldTable } from './components/field_table';
|
||||
import * as i18n from '../../translations';
|
||||
import { CategoriesSelector } from '../categories_selector';
|
||||
import { CategoriesBadges } from '../categories_badges';
|
||||
import { FieldTable } from '../field_table';
|
||||
|
||||
export type FieldBrowserModalProps = Pick<
|
||||
FieldBrowserProps,
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { omit } from 'lodash/fp';
|
||||
import { render } from '@testing-library/react';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiCheckbox,
|
||||
|
@ -15,7 +18,7 @@ import {
|
|||
EuiScreenReaderOnly,
|
||||
} from '@elastic/eui';
|
||||
import { uniqBy } from 'lodash/fp';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import { EcsFlat } from '@elastic/ecs';
|
||||
import { EcsMetadata } from '@kbn/alerts-as-data-utils/src/field_maps/types';
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { getFieldItemsData, getFieldColumns } from './field_items';
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { FieldName } from './field_name';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 { UseEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
|
@ -12,7 +15,7 @@ import {
|
|||
useEuiTheme,
|
||||
CriteriaWithPagination,
|
||||
} from '@elastic/eui';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import { getFieldColumns, getFieldItemsData } from '../field_items';
|
||||
import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from '../../helpers';
|
||||
import type { BrowserFieldItem, FieldBrowserProps, GetFieldTableColumns } from '../../types';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
|
||||
export const styles = {
|
||||
count: css`
|
||||
font-weight: bold;
|
||||
`,
|
||||
};
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
EuiText,
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { FieldTable } from './field_table';
|
||||
export type { FieldTableProps } from './field_table';
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { Search } from './search';
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { mockBrowserFields } from './mock';
|
||||
|
@ -15,7 +17,7 @@ import {
|
|||
filterBrowserFieldsByFieldName,
|
||||
filterSelectedBrowserFields,
|
||||
} from './helpers';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import { EcsFlat } from '@elastic/ecs';
|
||||
|
||||
describe('helpers', () => {
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { EcsMetadata } from '@kbn/alerts-as-data-utils/src/field_maps/types';
|
||||
|
@ -11,9 +13,9 @@ import {
|
|||
ALERT_MAINTENANCE_WINDOW_IDS,
|
||||
DefaultAlertFieldName,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { BrowserField, BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { BrowserField, BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { CASES, MAINTENANCE_WINDOWS } from '../translations';
|
||||
import { CASES, MAINTENANCE_WINDOWS } from './translations';
|
||||
|
||||
export const FIELD_BROWSER_WIDTH = 925;
|
||||
export const TABLE_HEIGHT = 260;
|
13
packages/response-ops/alerts_fields_browser/index.ts
Normal file
13
packages/response-ops/alerts_fields_browser/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { FieldBrowser } from './components/field_browser/field_browser';
|
||||
export type { FieldBrowserProps, FieldBrowserOptions } from './types';
|
||||
|
||||
export { FieldBrowser };
|
15
packages/response-ops/alerts_fields_browser/jest.config.js
Normal file
15
packages/response-ops/alerts_fields_browser/jest.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/response-ops/alerts_fields_browser'],
|
||||
setupFilesAfterEnv: ['<rootDir>/packages/response-ops/alerts_fields_browser/setup_tests.ts'],
|
||||
};
|
7
packages/response-ops/alerts_fields_browser/kibana.jsonc
Normal file
7
packages/response-ops/alerts_fields_browser/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/response-ops-alerts-fields-browser",
|
||||
"owner": "@elastic/response-ops",
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
|
@ -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 { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
|
||||
const DEFAULT_INDEX_PATTERN = [
|
||||
'apm-*-transaction*',
|
6
packages/response-ops/alerts_fields_browser/package.json
Normal file
6
packages/response-ops/alerts_fields_browser/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/response-ops-alerts-fields-browser",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
11
packages/response-ops/alerts_fields_browser/setup_tests.ts
Normal file
11
packages/response-ops/alerts_fields_browser/setup_tests.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import '@testing-library/jest-dom';
|
118
packages/response-ops/alerts_fields_browser/translations.ts
Normal file
118
packages/response-ops/alerts_fields_browser/translations.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const CASES = i18n.translate('responseOpsAlertsFieldsBrowser.cases.label', {
|
||||
defaultMessage: 'Cases',
|
||||
});
|
||||
|
||||
export const MAINTENANCE_WINDOWS = i18n.translate(
|
||||
'responseOpsAlertsFieldsBrowser.maintenanceWindows.label',
|
||||
{
|
||||
defaultMessage: 'Maintenance Windows',
|
||||
}
|
||||
);
|
||||
|
||||
export const CATEGORY = i18n.translate('responseOpsAlertsFieldsBrowser.categoryLabel', {
|
||||
defaultMessage: 'Category',
|
||||
});
|
||||
|
||||
export const CATEGORIES = i18n.translate('responseOpsAlertsFieldsBrowser.categoriesTitle', {
|
||||
defaultMessage: 'Categories',
|
||||
});
|
||||
|
||||
export const CATEGORIES_COUNT = (totalCount: number) =>
|
||||
i18n.translate('responseOpsAlertsFieldsBrowser.categoriesCountTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}',
|
||||
});
|
||||
|
||||
export const CLOSE = i18n.translate('responseOpsAlertsFieldsBrowser.closeButton', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
||||
|
||||
export const FIELDS_BROWSER = i18n.translate('responseOpsAlertsFieldsBrowser.fieldBrowserTitle', {
|
||||
defaultMessage: 'Fields',
|
||||
});
|
||||
|
||||
export const DESCRIPTION = i18n.translate('responseOpsAlertsFieldsBrowser.descriptionLabel', {
|
||||
defaultMessage: 'Description',
|
||||
});
|
||||
|
||||
export const DESCRIPTION_FOR_FIELD = (field: string) =>
|
||||
i18n.translate('responseOpsAlertsFieldsBrowser.descriptionForScreenReaderOnly', {
|
||||
values: {
|
||||
field,
|
||||
},
|
||||
defaultMessage: 'Description for field {field}:',
|
||||
});
|
||||
|
||||
export const NAME = i18n.translate('responseOpsAlertsFieldsBrowser.fieldName', {
|
||||
defaultMessage: 'Name',
|
||||
});
|
||||
|
||||
export const FIELD = i18n.translate('responseOpsAlertsFieldsBrowser.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
});
|
||||
|
||||
export const FIELDS = i18n.translate('responseOpsAlertsFieldsBrowser.fieldsTitle', {
|
||||
defaultMessage: 'Fields',
|
||||
});
|
||||
|
||||
export const FIELDS_SHOWING = i18n.translate('responseOpsAlertsFieldsBrowser.fieldsCountShowing', {
|
||||
defaultMessage: 'Showing',
|
||||
});
|
||||
|
||||
export const FIELDS_COUNT = (totalCount: number) =>
|
||||
i18n.translate('responseOpsAlertsFieldsBrowser.fieldsCountTitle', {
|
||||
values: { totalCount },
|
||||
defaultMessage: '{totalCount, plural, =1 {field} other {fields}}',
|
||||
});
|
||||
|
||||
export const FILTER_PLACEHOLDER = i18n.translate(
|
||||
'responseOpsAlertsFieldsBrowser.filterPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Field name',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_FIELDS_MATCH = i18n.translate('responseOpsAlertsFieldsBrowser.noFieldsMatchLabel', {
|
||||
defaultMessage: 'No fields match',
|
||||
});
|
||||
|
||||
export const NO_FIELDS_MATCH_INPUT = (searchInput: string) =>
|
||||
i18n.translate('responseOpsAlertsFieldsBrowser.noFieldsMatchInputLabel', {
|
||||
defaultMessage: 'No fields match {searchInput}',
|
||||
values: {
|
||||
searchInput,
|
||||
},
|
||||
});
|
||||
|
||||
export const RESET_FIELDS = i18n.translate('responseOpsAlertsFieldsBrowser.resetFieldsLink', {
|
||||
defaultMessage: 'Reset Fields',
|
||||
});
|
||||
|
||||
export const VIEW_COLUMN = (field: string) =>
|
||||
i18n.translate('responseOpsAlertsFieldsBrowser.viewColumnCheckboxAriaLabel', {
|
||||
values: { field },
|
||||
defaultMessage: 'View {field} column',
|
||||
});
|
||||
|
||||
export const VIEW_LABEL = i18n.translate('responseOpsAlertsFieldsBrowser.viewLabel', {
|
||||
defaultMessage: 'View',
|
||||
});
|
||||
|
||||
export const VIEW_VALUE_SELECTED = i18n.translate('responseOpsAlertsFieldsBrowser.viewSelected', {
|
||||
defaultMessage: 'selected',
|
||||
});
|
||||
|
||||
export const VIEW_VALUE_ALL = i18n.translate('responseOpsAlertsFieldsBrowser.viewAll', {
|
||||
defaultMessage: 'all',
|
||||
});
|
24
packages/response-ops/alerts_fields_browser/tsconfig.json
Normal file
24
packages/response-ops/alerts_fields_browser/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
"@kbn/rule-registry-plugin",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/rule-data-utils",
|
||||
]
|
||||
}
|
|
@ -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 type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
|
||||
/**
|
||||
* An item rendered in the table
|
175
packages/response-ops/alerts_table/README.md
Normal file
175
packages/response-ops/alerts_table/README.md
Normal file
|
@ -0,0 +1,175 @@
|
|||
# @kbn/response-ops-alerts-table
|
||||
|
||||
An abstraction on top of `EuiDataGrid` dedicated to rendering alert documents.
|
||||
|
||||
## Usage
|
||||
|
||||
In addition to `EuiDataGrid`'s functionality, the table manages the paginated and cached fetching of alerts, based on
|
||||
the provided `ruleTypeIds` and `consumers` (the final query can be refined through the `query` and `initialSort` props).
|
||||
The `id` prop is used to persist the table state in `localStorage`.
|
||||
|
||||
```tsx
|
||||
<AlertsTable
|
||||
id="my-alerts-table"
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
consumers={consumers}
|
||||
query={esQuery}
|
||||
initialSort={defaultAlertsTableSort}
|
||||
renderCellValue={renderCellValue}
|
||||
renderActionsCell={AlertActionsCell}
|
||||
services={{ ... }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Columns
|
||||
|
||||
Just like in `EuiDataGrid`, the columns are customizable through the `columns` prop. In addition to those, the table
|
||||
renders an "Actions" column with default alert call-to-actions and provides row selection and bulk actions
|
||||
functionality.
|
||||
|
||||
```tsx
|
||||
// The @kbn/rule-data-utils package exports constants
|
||||
// for many common alert field keys
|
||||
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
|
||||
|
||||
<AlertsTable columns={[
|
||||
{
|
||||
displayAsText: 'Rule',
|
||||
id: ALERT_RULE_NAME,
|
||||
initialWidth: 230,
|
||||
},
|
||||
]} />
|
||||
```
|
||||
|
||||
## Cells, popovers and flyouts
|
||||
|
||||
All the sub-components of the table are customizable through the `render*`
|
||||
props (`renderCellValue`, `renderCellPopover`, `renderActionsCell`, etc.). Values passed to these props are treated as
|
||||
components, allowing hooks, context, and other React concepts to be used.
|
||||
|
||||
```tsx
|
||||
const CustomCellValue: GetAlertsTableProp<'renderCellValue'> = ({ alert }) => {
|
||||
// ...
|
||||
};
|
||||
|
||||
<AlertsTable renderCellValue={CustomCellValue} />
|
||||
```
|
||||
|
||||
## Render context
|
||||
|
||||
All the sub-component renderers receive as part of their props a context object (
|
||||
see [EuiDataGrid's `cellContext`](https://eui.elastic.co/#/tabular-content/data-grid-cells-popovers%23cell-context))
|
||||
with common utilities and services (i.e. the fetched alerts, loading states etc.).
|
||||
You can add properties to this context by means of the `additionalContext` prop:
|
||||
|
||||
```tsx
|
||||
<AlertsTable
|
||||
additionalContext={{
|
||||
myCustomProperty: 'my custom value',
|
||||
}}
|
||||
renderCellValue={({ myCustomProperty /*string*/ }) => {
|
||||
// ...
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
The context type is inferred based on the `additionalContext` prop and all render functions props are typed accordingly.
|
||||
To avoid prop drilling, you can use the `useAlertsTableContext` hook to access the same context in any sub-component.
|
||||
|
||||
```tsx
|
||||
const CustomCellValue = ({ alert }) => {
|
||||
const { alertsCount, myCustomProperty } = useAlertsTableContext<MyAdditionalContext>();
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
In order to define your custom sub-components separately from the table but still benefit from the context type
|
||||
inference, you may want to extract props from the `AlertsTableProps` type. The `GetAlertsTableProp` utility type is
|
||||
provided for this: it extracts the type of a specific prop from the `AlertsTableProps` type, excluding `undefined` in
|
||||
case of optional props.
|
||||
|
||||
```tsx
|
||||
import type { GetAlertsTableProp } from '@kbn/response-ops-alerts-table/types';
|
||||
|
||||
export const CustomCellValue: GetAlertsTableProp<'renderCellValue'> = ({ alert }) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
If you also have an additional context, you can define a type for it and wrap the `AlertsTableProps`, providing it as a
|
||||
generic:
|
||||
|
||||
```tsx
|
||||
import type { AlertsTableProps } from '@kbn/response-ops-alerts-table/types';
|
||||
|
||||
interface MyAdditionalContext {
|
||||
myCustomProperty: string;
|
||||
}
|
||||
|
||||
export type MyAlertsTableProps = AlertsTableProps<MyAdditionalContext>;
|
||||
export type GetMyAlertsTableProp<K extends keyof MyAdditionalContext> = Exclude<MyAlertsTableProps[K], undefined>;
|
||||
|
||||
export const CustomCellValue: GetMyAlertsTableProp<'renderCellValue'> = ({ myCustomProperty }) => {
|
||||
// ...
|
||||
};
|
||||
|
||||
<AlertsTable<MyAdditionalContext>
|
||||
additionalContext={{
|
||||
myCustomProperty: 'my custom value',
|
||||
}}
|
||||
renderCellValue={CustomCellValue}
|
||||
/>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
The table relies on the following Kibana services, expected in the `services` prop:
|
||||
|
||||
- `data`
|
||||
- `http`
|
||||
- `notifications`
|
||||
- `fieldFormats`
|
||||
- `application`
|
||||
- `licensing`
|
||||
- `settings`
|
||||
- `cases` (optional)
|
||||
|
||||
## Integrations
|
||||
|
||||
The table has built-in integration with Maintenance Windows and Cases. If alerts have maintenance windows or cases
|
||||
associated to them, they will be loaded and made available through the `maintenanceWindows` and `cases` properties of
|
||||
the render context.
|
||||
A special cell renderer is used by default for the `kibana.alert.maintenance_window_ids` and `kibana.alert.case_ids`
|
||||
columns.
|
||||
|
||||
## Lazy loading
|
||||
|
||||
Contrary to the previous implementation exported by `triggersActionsUI`, this package doesn't prescribe how to lazy load
|
||||
the table component; a default export is just provided for convenience. However, do consider that
|
||||
the `React.lazy` function loses the original generic types of the component. To make the type inference work correctly,
|
||||
you can assert the type of the lazy loaded component using a type import:
|
||||
|
||||
```tsx
|
||||
import type { AlertsTable as AlertsTableType } from '@kbn/response-ops-alerts-table';
|
||||
|
||||
const AlertsTable = React.lazy(() => import('@kbn/response-ops-alerts-table')) as AlertsTableType;
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
When mocking the table, keep in mind that the component is manually typed as a normal function component (to keep its
|
||||
generic types), but it's actually a memoized, forwardRef'ed component. To mock it properly, mock the entire module:
|
||||
|
||||
```tsx
|
||||
jest.mock('@kbn/response-ops-alerts-table', () => ({
|
||||
AlertsTable: jest.fn().mockImplementation(() => <div data-test-subj="alerts-table"/>),
|
||||
}));
|
||||
```
|
||||
|
||||
## What's new compared to `triggersActionsUI`?
|
||||
|
||||
- The alerts table registry was removed. The table is now a standalone component and the configuration is based entirely
|
||||
on props.
|
||||
- All the custom renderers (cell, cell popover, actions cell, etc.) are now exposed as strongly typed props (`render*`).
|
||||
- More `EuiDataGrid` props are exposed for customization.
|
|
@ -1,14 +1,16 @@
|
|||
/*
|
||||
* 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 { bulkGetCases } from './bulk_get_cases';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
describe('Bulk Get Cases API', () => {
|
||||
describe('bulkGetCases', () => {
|
||||
const abortCtrl = new AbortController();
|
||||
const mockCoreSetup = coreMock.createSetup();
|
||||
const http = mockCoreSetup.http;
|
|
@ -1,12 +1,15 @@
|
|||
/*
|
||||
* 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 { CaseStatuses } from '@kbn/cases-components';
|
||||
import { HttpStart } from '@kbn/core-http-browser';
|
||||
|
||||
const INTERNAL_BULK_GET_CASES_URL = '/internal/cases/_bulk_get';
|
||||
|
||||
export interface Case {
|
||||
|
@ -37,15 +40,9 @@ export interface CasesBulkGetResponse {
|
|||
}>;
|
||||
}
|
||||
|
||||
export const bulkGetCases = async (
|
||||
http: HttpStart,
|
||||
params: { ids: string[] },
|
||||
signal?: AbortSignal
|
||||
): Promise<CasesBulkGetResponse> => {
|
||||
const res = await http.post<CasesBulkGetResponse>(INTERNAL_BULK_GET_CASES_URL, {
|
||||
export const bulkGetCases = (http: HttpStart, params: { ids: string[] }, signal?: AbortSignal) => {
|
||||
return http.post<CasesBulkGetResponse>(INTERNAL_BULK_GET_CASES_URL, {
|
||||
body: JSON.stringify({ ...params }),
|
||||
signal,
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
|
@ -1,14 +1,16 @@
|
|||
/*
|
||||
* 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 { coreMock } from '@kbn/core/public/mocks';
|
||||
import { bulkGetMaintenanceWindows } from './bulk_get_maintenance_windows';
|
||||
|
||||
describe('Bulk Get Maintenance Windows API', () => {
|
||||
describe('bulkGetMaintenanceWindows', () => {
|
||||
const mockCoreSetup = coreMock.createSetup();
|
||||
const http = mockCoreSetup.http;
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
/*
|
||||
* 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 { HttpStart } from '@kbn/core-http-browser';
|
||||
import {
|
||||
MaintenanceWindow,
|
||||
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { AsApiContract } from '@kbn/actions-plugin/common';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { MaintenanceWindow } from '@kbn/alerting-plugin/common';
|
||||
import type { AsApiContract } from '@kbn/actions-plugin/common';
|
||||
import { INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH } from '../constants';
|
||||
|
||||
export interface BulkGetMaintenanceWindowsParams {
|
||||
http: HttpStart;
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 React, { ComponentProps, useEffect } from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { ActionsCellHost } from './actions_cell_host';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
import { mockRenderContext } from '../mocks/context.mock';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
|
||||
const props = createPartialObjectMock<ComponentProps<typeof ActionsCellHost>>({
|
||||
...mockRenderContext,
|
||||
rowIndex: 0,
|
||||
});
|
||||
|
||||
describe('ActionsCellHost', () => {
|
||||
it('should render the provided custom actions cell', () => {
|
||||
render(
|
||||
<ActionsCellHost
|
||||
{...props}
|
||||
renderActionsCell={jest.fn(() => (
|
||||
<div data-test-subj="renderActionsCell" />
|
||||
))}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('renderActionsCell')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should catch errors from the custom actions cell', async () => {
|
||||
const CustomActionsCell = () => {
|
||||
useEffect(() => {
|
||||
throw new Error('test error');
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ActionsCellHost {...props} renderActionsCell={CustomActionsCell} />
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(await screen.findByTestId('errorCell')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { SetRequired } from 'type-fest';
|
||||
import React, { ComponentProps, useCallback } from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { typedMemo } from '../utils/react';
|
||||
import { AdditionalContext, AlertsTableProps, BulkActionsVerbs } from '../types';
|
||||
import { ErrorBoundary } from './error_boundary';
|
||||
import { ErrorCell } from './error_cell';
|
||||
|
||||
/**
|
||||
* Entry point for rendering actions cells (in control columns)
|
||||
*/
|
||||
export const ActionsCellHost = typedMemo(
|
||||
<AC extends AdditionalContext>(
|
||||
// The actions control column is active only when the user provided the `renderActionsCell` prop
|
||||
props: SetRequired<
|
||||
ComponentProps<NonNullable<AlertsTableProps<AC>['renderActionsCell']>>,
|
||||
'renderActionsCell'
|
||||
>
|
||||
) => {
|
||||
const {
|
||||
rowIndex,
|
||||
pageSize,
|
||||
pageIndex,
|
||||
alerts,
|
||||
oldAlertsData,
|
||||
ecsAlertsData,
|
||||
bulkActionsStore,
|
||||
renderActionsCell: ActionsCell,
|
||||
visibleRowIndex,
|
||||
} = props;
|
||||
const idx = rowIndex - pageSize * pageIndex;
|
||||
const alert = alerts[idx];
|
||||
const legacyAlert = oldAlertsData[idx];
|
||||
const ecsAlert = ecsAlertsData[idx];
|
||||
const [, updateBulkActionsState] = bulkActionsStore;
|
||||
|
||||
const setIsActionLoading = useCallback(
|
||||
(_isLoading: boolean = true) => {
|
||||
updateBulkActionsState({
|
||||
action: BulkActionsVerbs.updateRowLoadingState,
|
||||
rowIndex: visibleRowIndex,
|
||||
isLoading: _isLoading,
|
||||
});
|
||||
},
|
||||
[visibleRowIndex, updateBulkActionsState]
|
||||
);
|
||||
|
||||
if (!alert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<ErrorBoundary fallback={ErrorCell}>
|
||||
<ActionsCell
|
||||
{...(props as ComponentProps<NonNullable<AlertsTableProps<AC>['renderActionsCell']>>)}
|
||||
alert={alert}
|
||||
legacyAlert={legacyAlert}
|
||||
ecsAlert={ecsAlert}
|
||||
setIsActionLoading={setIsActionLoading}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -1,72 +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; 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 React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Alert } from '@kbn/alerting-types';
|
||||
import { AlertLifecycleStatusCell } from './alert_lifecycle_status_cell';
|
||||
import { CellComponentProps } from '../types';
|
||||
import { Alert } from '../../../../types';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../test_utils';
|
||||
import { getCasesMockMap } from '../cases/index.mock';
|
||||
import { getMaintenanceWindowMockMap } from '../maintenance_windows/index.mock';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
import { getCasesMapMock } from '../mocks/cases.mock';
|
||||
import { getMaintenanceWindowsMapMock } from '../mocks/maintenance_windows.mock';
|
||||
|
||||
describe('AlertLifecycleStatusCell', () => {
|
||||
const casesMap = getCasesMockMap();
|
||||
const maintenanceWindowsMap = getMaintenanceWindowMockMap();
|
||||
const alert = {
|
||||
const casesMap = getCasesMapMock();
|
||||
const maintenanceWindowsMap = getMaintenanceWindowsMapMock();
|
||||
const alert: Alert = {
|
||||
_id: 'alert-id',
|
||||
_index: 'alert-index',
|
||||
'kibana.alert.status': ['active'],
|
||||
} as Alert;
|
||||
};
|
||||
|
||||
const props: CellComponentProps = {
|
||||
const props = {
|
||||
isLoading: false,
|
||||
alert,
|
||||
cases: casesMap,
|
||||
maintenanceWindows: maintenanceWindowsMap,
|
||||
columnId: 'kibana.alert.status',
|
||||
showAlertStatusWithFlapping: true,
|
||||
};
|
||||
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
// Assertion used to avoid defining all the (numerous) context properties
|
||||
} as CellComponentProps;
|
||||
|
||||
it('shows the status', async () => {
|
||||
appMockRender.render(<AlertLifecycleStatusCell {...props} />);
|
||||
render(<AlertLifecycleStatusCell {...props} />);
|
||||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not shows the status if showAlertStatusWithFlapping=false', async () => {
|
||||
appMockRender.render(
|
||||
<AlertLifecycleStatusCell {...props} showAlertStatusWithFlapping={false} />
|
||||
);
|
||||
render(<AlertLifecycleStatusCell {...props} showAlertStatusWithFlapping={false} />);
|
||||
expect(screen.queryByText('Active')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the status with flapping', async () => {
|
||||
appMockRender.render(
|
||||
render(
|
||||
<AlertLifecycleStatusCell
|
||||
{...props}
|
||||
alert={{ ...alert, 'kibana.alert.flapping': ['true'] } as Alert}
|
||||
alert={{ ...alert, 'kibana.alert.flapping': ['true'] }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Flapping')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the status with multiple values', async () => {
|
||||
appMockRender.render(
|
||||
render(
|
||||
<AlertLifecycleStatusCell
|
||||
{...props}
|
||||
alert={{ ...alert, 'kibana.alert.status': ['active', 'recovered'] } as Alert}
|
||||
alert={{ ...alert, 'kibana.alert.status': ['active', 'recovered'] }}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -74,12 +66,7 @@ describe('AlertLifecycleStatusCell', () => {
|
|||
});
|
||||
|
||||
it('shows the default cell if the status is empty', async () => {
|
||||
appMockRender.render(
|
||||
<AlertLifecycleStatusCell
|
||||
{...props}
|
||||
alert={{ ...alert, 'kibana.alert.status': [] } as unknown as Alert}
|
||||
/>
|
||||
);
|
||||
render(<AlertLifecycleStatusCell {...props} alert={{ ...alert, 'kibana.alert.status': [] }} />);
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument();
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 { AlertStatus, ALERT_FLAPPING, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
|
@ -10,12 +12,12 @@ import React, { memo } from 'react';
|
|||
import { EuiBadge, EuiFlexGroup, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { AlertLifecycleStatusBadge } from '../../../components/alert_lifecycle_status_badge';
|
||||
import { CellComponentProps } from '../types';
|
||||
import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared';
|
||||
import { DefaultCell } from './default_cell';
|
||||
import { useAlertMutedState } from '../hooks/alert_mute/use_alert_muted_state';
|
||||
import { useAlertMutedState } from '../hooks/use_alert_muted_state';
|
||||
import type { CellComponent } from '../types';
|
||||
|
||||
const AlertLifecycleStatusCellComponent: React.FC<CellComponentProps> = (props) => {
|
||||
export const AlertLifecycleStatusCell: CellComponent = memo((props) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { alert, showAlertStatusWithFlapping } = props;
|
||||
const { isMuted } = useAlertMutedState(alert);
|
||||
|
@ -24,16 +26,16 @@ const AlertLifecycleStatusCellComponent: React.FC<CellComponentProps> = (props)
|
|||
return null;
|
||||
}
|
||||
|
||||
const alertStatus = (alert && alert[ALERT_STATUS]) ?? [];
|
||||
const alertStatus = (alert?.[ALERT_STATUS] ?? []) as string[] | undefined;
|
||||
|
||||
if (Array.isArray(alertStatus) && alertStatus.length) {
|
||||
const flapping = (alert && alert[ALERT_FLAPPING]) ?? [];
|
||||
const flapping = alert?.[ALERT_FLAPPING]?.[0] as boolean | undefined;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<AlertLifecycleStatusBadge
|
||||
alertStatus={alertStatus.join() as AlertStatus}
|
||||
flapping={flapping[0]}
|
||||
flapping={flapping}
|
||||
/>
|
||||
{isMuted && (
|
||||
<EuiToolTip
|
||||
|
@ -54,8 +56,4 @@ const AlertLifecycleStatusCellComponent: React.FC<CellComponentProps> = (props)
|
|||
}
|
||||
|
||||
return <DefaultCell {...props} />;
|
||||
};
|
||||
|
||||
AlertLifecycleStatusCellComponent.displayName = 'AlertLifecycleStatusCell';
|
||||
|
||||
export const AlertLifecycleStatusCell = memo(AlertLifecycleStatusCellComponent);
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React, { useMemo } from 'react';
|
|
@ -0,0 +1,496 @@
|
|||
/*
|
||||
* 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 React, { FunctionComponent, useMemo, useReducer } from 'react';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
import { AlertsDataGrid } from './alerts_data_grid';
|
||||
import { AlertsDataGridProps, BulkActionsState } from '../types';
|
||||
import { AdditionalContext, RenderContext } from '../types';
|
||||
import { EuiButton, EuiButtonIcon, EuiDataGridColumnCellAction, EuiFlexItem } from '@elastic/eui';
|
||||
import { bulkActionsReducer } from '../reducers/bulk_actions_reducer';
|
||||
import { getJsDomPerformanceFix } from '../utils/test';
|
||||
import { useCaseViewNavigation } from '../hooks/use_case_view_navigation';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { AlertsTableContextProvider } from '../contexts/alerts_table_context';
|
||||
import {
|
||||
mockRenderContext,
|
||||
mockColumns,
|
||||
mockAlerts,
|
||||
createMockBulkActionsState,
|
||||
} from '../mocks/context.mock';
|
||||
import {
|
||||
CELL_ACTIONS_EXPAND_TEST_ID,
|
||||
CELL_ACTIONS_POPOVER_TEST_ID,
|
||||
FIELD_BROWSER_BTN_TEST_ID,
|
||||
FIELD_BROWSER_CUSTOM_CREATE_BTN_TEST_ID,
|
||||
FIELD_BROWSER_TEST_ID,
|
||||
} from '../constants';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { testQueryClientConfig } from '@kbn/alerts-ui-shared/src/common/test_utils/test_query_client_config';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
|
||||
|
||||
jest.mock('../hooks/use_case_view_navigation');
|
||||
|
||||
const cellActionOnClickMockedFn = jest.fn();
|
||||
|
||||
const { fix, cleanup } = getJsDomPerformanceFix();
|
||||
|
||||
beforeAll(() => {
|
||||
fix();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
export type BaseAlertsDataGridProps = AlertsDataGridProps;
|
||||
export type TestAlertsDataGridProps = Partial<Omit<BaseAlertsDataGridProps, 'renderContext'>> & {
|
||||
renderContext?: Partial<RenderContext<AdditionalContext>>;
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient(testQueryClientConfig);
|
||||
|
||||
export const mockDataGridProps: Partial<BaseAlertsDataGridProps> = {
|
||||
pageSizeOptions: [1, 10, 20, 50, 100],
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: [],
|
||||
visibleColumns: mockColumns.map((c) => c.id),
|
||||
'data-test-subj': 'testTable',
|
||||
onToggleColumn: jest.fn(),
|
||||
onResetColumns: jest.fn(),
|
||||
onChangeVisibleColumns: jest.fn(),
|
||||
query: {},
|
||||
sort: [],
|
||||
alertsQuerySnapshot: { request: [], response: [] },
|
||||
onSortChange: jest.fn(),
|
||||
onChangePageIndex: jest.fn(),
|
||||
onChangePageSize: jest.fn(),
|
||||
getBulkActions: () => [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
label: 'Fake Bulk Action',
|
||||
key: 'fakeBulkAction',
|
||||
'data-test-subj': 'fake-bulk-action',
|
||||
disableOnQuery: false,
|
||||
onClick: () => {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fieldsBrowserOptions: {
|
||||
createFieldButton: () => <EuiButton data-test-subj={FIELD_BROWSER_CUSTOM_CREATE_BTN_TEST_ID} />,
|
||||
},
|
||||
casesConfiguration: { featureId: 'test-feature-id', owner: ['cases'] },
|
||||
};
|
||||
|
||||
describe('AlertsDataGrid', () => {
|
||||
const useCaseViewNavigationMock = useCaseViewNavigation as jest.Mock;
|
||||
useCaseViewNavigationMock.mockReturnValue({ navigateToCaseView: jest.fn() });
|
||||
|
||||
const TestComponent: React.FunctionComponent<
|
||||
Omit<TestAlertsDataGridProps, 'renderContext'> & {
|
||||
initialBulkActionsState?: BulkActionsState;
|
||||
renderContext?: Partial<RenderContext<AdditionalContext>>;
|
||||
}
|
||||
> = (props) => {
|
||||
const bulkActionsStore = useReducer(
|
||||
bulkActionsReducer,
|
||||
props.initialBulkActionsState || createMockBulkActionsState()
|
||||
);
|
||||
const renderContext = useMemo(
|
||||
() => ({
|
||||
...mockRenderContext,
|
||||
bulkActionsStore,
|
||||
...props.renderContext,
|
||||
}),
|
||||
[bulkActionsStore, props.renderContext]
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertsTableContextProvider value={renderContext}>
|
||||
<QueryClientProvider client={queryClient} context={AlertsQueryContext}>
|
||||
<IntlProvider locale="en">
|
||||
<AlertsDataGrid {...({ ...props, renderContext } as BaseAlertsDataGridProps)} />
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
</AlertsTableContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Alerts table UI', () => {
|
||||
it('should support sorting', async () => {
|
||||
const { container } = render(<TestComponent {...mockDataGridProps} />);
|
||||
await userEvent.click(container.querySelector('.euiDataGridHeaderCell__button')!, {
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByTestId(`dataGridHeaderCellActionGroup-${mockColumns[0].id}`),
|
||||
{
|
||||
pointerEventsCheck: 0,
|
||||
}
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTitle('Sort A-Z'), {
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
|
||||
expect(mockDataGridProps.onSortChange).toHaveBeenCalledWith([
|
||||
{ direction: 'asc', id: 'kibana.alert.rule.name' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support pagination', async () => {
|
||||
render(<TestComponent {...mockDataGridProps} />);
|
||||
await userEvent.click(screen.getByTestId('pagination-button-1'), {
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
|
||||
expect(mockDataGridProps.onChangePageIndex).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should show when it was updated', () => {
|
||||
render(<TestComponent {...mockDataGridProps} />);
|
||||
expect(screen.getByTestId('toolbar-updated-at')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should show alerts count', () => {
|
||||
render(<TestComponent {...mockDataGridProps} />);
|
||||
expect(screen.getByTestId('toolbar-alerts-count')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('should show alert status', () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...mockDataGridProps}
|
||||
renderContext={{
|
||||
renderCellValue: undefined,
|
||||
pageSize: 10,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryAllByTestId('alertLifecycleStatusBadge')[0].textContent).toEqual(
|
||||
'Flapping'
|
||||
);
|
||||
expect(screen.queryAllByTestId('alertLifecycleStatusBadge')[1].textContent).toEqual('Active');
|
||||
expect(screen.queryAllByTestId('alertLifecycleStatusBadge')[2].textContent).toEqual(
|
||||
'Recovered'
|
||||
);
|
||||
expect(screen.queryAllByTestId('alertLifecycleStatusBadge')[3].textContent).toEqual(
|
||||
'Recovered'
|
||||
);
|
||||
});
|
||||
|
||||
describe('leading control columns', () => {
|
||||
it('should render other leading controls', () => {
|
||||
const props: TestAlertsDataGridProps = {
|
||||
...mockDataGridProps,
|
||||
leadingControlColumns: [
|
||||
{
|
||||
id: 'selection',
|
||||
width: 67,
|
||||
headerCellRender: () => <span data-test-subj="testHeader">Test header</span>,
|
||||
rowCellRender: () => <h2 data-test-subj="testCell">Test cell</h2>,
|
||||
},
|
||||
],
|
||||
};
|
||||
render(<TestComponent {...props} />);
|
||||
expect(screen.queryByTestId('testHeader')).not.toBe(null);
|
||||
expect(screen.queryByTestId('testCell')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions column', () => {
|
||||
it('should render custom actions cells', () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...mockDataGridProps}
|
||||
renderContext={{
|
||||
renderActionsCell: () => (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="analyzeEvent"
|
||||
color="primary"
|
||||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testAction"
|
||||
aria-label="testActionLabel"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="analyzeEvent"
|
||||
color="primary"
|
||||
onClick={() => {}}
|
||||
size="s"
|
||||
data-test-subj="testAction2"
|
||||
aria-label="testActionLabel2"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('testAction')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('testAction2')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('expandColumnCellOpenFlyoutButton-0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no action column if neither the action nor the expand action config is set', () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...mockDataGridProps}
|
||||
renderContext={{
|
||||
renderActionsCell: undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('expandColumnHeaderLabel')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('expandColumnCellOpenFlyoutButton')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('row loading state on action', () => {
|
||||
type ExtractFunctionComponent<T> = T extends FunctionComponent<infer P> ? T : never;
|
||||
const mockRenderActionsCell = jest.fn(
|
||||
mockRenderContext.renderActionsCell as ExtractFunctionComponent<
|
||||
typeof mockRenderContext.renderActionsCell
|
||||
>
|
||||
);
|
||||
const props: TestAlertsDataGridProps = {
|
||||
...mockDataGridProps,
|
||||
actionsColumnWidth: 124,
|
||||
renderContext: {
|
||||
pageSize: 10,
|
||||
renderActionsCell: mockRenderActionsCell,
|
||||
},
|
||||
};
|
||||
|
||||
it('should show the row loader when callback triggered', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
fireEvent.click((await screen.findAllByTestId('testAction'))[0]);
|
||||
|
||||
// the callback given to our clients to run when they want to update the loading state
|
||||
act(() => {
|
||||
mockRenderActionsCell.mock.calls[0][0].setIsActionLoading!(true);
|
||||
});
|
||||
|
||||
expect(await screen.findAllByTestId('row-loader')).toHaveLength(1);
|
||||
const selectedOptions = await screen.findAllByTestId('dataGridRowCell');
|
||||
|
||||
// first row, first column
|
||||
expect(within(selectedOptions[0]).getByLabelText('Loading')).toBeDefined();
|
||||
expect(
|
||||
within(selectedOptions[0]).queryByTestId('bulk-actions-row-cell')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// second row, first column
|
||||
expect(within(selectedOptions[6]).queryByLabelText('Loading')).not.toBeInTheDocument();
|
||||
expect(within(selectedOptions[6]).getByTestId('bulk-actions-row-cell')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show the row loader when callback triggered with false', async () => {
|
||||
const initialBulkActionsState = {
|
||||
...createMockBulkActionsState(),
|
||||
rowSelection: new Map([[0, { isLoading: true }]]),
|
||||
};
|
||||
|
||||
render(<TestComponent {...props} initialBulkActionsState={initialBulkActionsState} />);
|
||||
fireEvent.click((await screen.findAllByTestId('testAction'))[0]);
|
||||
|
||||
// the callback given to our clients to run when they want to update the loading state
|
||||
act(() => {
|
||||
mockRenderActionsCell.mock.calls[0][0].setIsActionLoading!(false);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('row-loader')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cell Actions', () => {
|
||||
const mockGetCellActionsForColumn = jest.fn(
|
||||
(columnId: string): EuiDataGridColumnCellAction[] => [
|
||||
({ rowIndex, Component }) => {
|
||||
const label = 'Fake Cell First Action';
|
||||
return (
|
||||
<Component
|
||||
onClick={() => cellActionOnClickMockedFn(columnId, rowIndex)}
|
||||
data-test-subj={'fake-cell-first-action'}
|
||||
iconType="refresh"
|
||||
aria-label={label}
|
||||
/>
|
||||
);
|
||||
},
|
||||
]
|
||||
);
|
||||
const props: TestAlertsDataGridProps = {
|
||||
...mockDataGridProps,
|
||||
cellActionsOptions: {
|
||||
getCellActionsForColumn: mockGetCellActionsForColumn,
|
||||
visibleCellActions: 2,
|
||||
disabledCellActions: [],
|
||||
},
|
||||
};
|
||||
|
||||
it('should render cell actions on hover', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
|
||||
const reasonFirstRow = (await screen.findAllByTestId('dataGridRowCell'))[3];
|
||||
|
||||
fireEvent.mouseOver(reasonFirstRow);
|
||||
|
||||
expect(await screen.findByTestId('fake-cell-first-action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render expandable cell actions', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
const reasonFirstRow = (await screen.findAllByTestId('dataGridRowCell'))[3];
|
||||
|
||||
fireEvent.mouseOver(reasonFirstRow);
|
||||
|
||||
expect(await screen.findByTestId(CELL_ACTIONS_EXPAND_TEST_ID)).toBeVisible();
|
||||
|
||||
fireEvent.click(await screen.findByTestId(CELL_ACTIONS_EXPAND_TEST_ID));
|
||||
|
||||
expect(await screen.findByTestId(CELL_ACTIONS_POPOVER_TEST_ID)).toBeVisible();
|
||||
expect(await screen.findAllByLabelText(/fake cell first action/i)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fields browser', () => {
|
||||
it('fields browser is working correctly', async () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...mockDataGridProps}
|
||||
initialBulkActionsState={{
|
||||
...createMockBulkActionsState(),
|
||||
rowSelection: new Map(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const fieldBrowserBtn = screen.getByTestId(FIELD_BROWSER_BTN_TEST_ID);
|
||||
expect(fieldBrowserBtn).toBeVisible();
|
||||
|
||||
fireEvent.click(fieldBrowserBtn);
|
||||
|
||||
expect(await screen.findByTestId(FIELD_BROWSER_TEST_ID)).toBeVisible();
|
||||
|
||||
expect(await screen.findByTestId(FIELD_BROWSER_CUSTOM_CREATE_BTN_TEST_ID)).toBeVisible();
|
||||
});
|
||||
|
||||
it('syncs the columns state correctly between the column selector and the field selector', async () => {
|
||||
const columnToHide = mockColumns[0];
|
||||
render(
|
||||
<TestComponent
|
||||
{...mockDataGridProps}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: true,
|
||||
}}
|
||||
initialBulkActionsState={{
|
||||
...createMockBulkActionsState(),
|
||||
rowSelection: new Map(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const fieldBrowserBtn = await screen.findByTestId(FIELD_BROWSER_BTN_TEST_ID);
|
||||
const columnSelectorBtn = await screen.findByTestId('dataGridColumnSelectorButton');
|
||||
|
||||
// Open the column visibility selector and hide the column
|
||||
fireEvent.click(columnSelectorBtn);
|
||||
const columnVisibilityToggle = await screen.findByTestId(
|
||||
`dataGridColumnSelectorToggleColumnVisibility-${columnToHide.id}`
|
||||
);
|
||||
fireEvent.click(columnVisibilityToggle);
|
||||
|
||||
// Open the field browser
|
||||
fireEvent.click(fieldBrowserBtn);
|
||||
expect(await screen.findByTestId(FIELD_BROWSER_TEST_ID)).toBeVisible();
|
||||
|
||||
// The column should be checked in the field browser, independent of its visibility status
|
||||
const columnCheckbox: HTMLInputElement = await screen.findByTestId(
|
||||
`field-${columnToHide.id}-checkbox`
|
||||
);
|
||||
expect(columnCheckbox).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cases column', () => {
|
||||
const props: TestAlertsDataGridProps = {
|
||||
...mockDataGridProps,
|
||||
renderContext: {
|
||||
pageSize: mockAlerts.length,
|
||||
},
|
||||
};
|
||||
|
||||
it('should show the cases column', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
expect(await screen.findByText('Cases')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the cases titles correctly', async () => {
|
||||
render(<TestComponent {...props} renderContext={{ pageSize: 10 }} />);
|
||||
expect(await screen.findByText('Test case')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Test case 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show loading skeleton if it loads cases', async () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...props}
|
||||
renderContext={{
|
||||
pageSize: 10,
|
||||
isLoading: true,
|
||||
isLoadingCases: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect((await screen.findAllByTestId('cases-cell-loading')).length).toBe(4);
|
||||
});
|
||||
|
||||
it('shows the cases tooltip', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
expect(await screen.findByText('Test case')).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(screen.getByText('Test case'));
|
||||
|
||||
expect(await screen.findByTestId('cases-components-tooltip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic row height mode', () => {
|
||||
it('should render a non-virtualized grid body when the dynamicRowHeight option is on', async () => {
|
||||
const { container } = render(<TestComponent {...mockDataGridProps} dynamicRowHeight />);
|
||||
|
||||
expect(container.querySelector('.euiDataGrid__customRenderBody')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render a virtualized grid body when the dynamicRowHeight option is off', async () => {
|
||||
const { container } = render(<TestComponent {...mockDataGridProps} />);
|
||||
|
||||
expect(container.querySelector('.euiDataGrid__virtualized')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
* 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 React, { FC, lazy, Suspense, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridControlColumn,
|
||||
EuiDataGridProps,
|
||||
EuiDataGridStyle,
|
||||
RenderCellValue,
|
||||
tint,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { ActionsCellHost } from './actions_cell_host';
|
||||
import { ControlColumnHeaderCell } from './control_column_header_cell';
|
||||
import { CellValueHost } from './cell_value_host';
|
||||
import { BulkActionsCell } from './bulk_actions_cell';
|
||||
import { BulkActionsHeader } from './bulk_actions_header_cell';
|
||||
import { AdditionalContext, AlertsDataGridProps, CellActionsOptions } from '../types';
|
||||
import { useGetToolbarVisibility } from '../hooks/use_toolbar_visibility';
|
||||
import { InspectButtonContainer } from './alerts_query_inspector';
|
||||
import { typedMemo } from '../utils/react';
|
||||
import type { AlertsFlyout as AlertsFlyoutType } from './alerts_flyout';
|
||||
import { useBulkActions } from '../hooks/use_bulk_actions';
|
||||
import { useSorting } from '../hooks/use_sorting';
|
||||
import { CellPopoverHost } from './cell_popover_host';
|
||||
import { NonVirtualizedGridBody } from './non_virtualized_grid_body';
|
||||
|
||||
const AlertsFlyout = lazy(() => import('./alerts_flyout')) as typeof AlertsFlyoutType;
|
||||
|
||||
const defaultGridStyle: EuiDataGridStyle = {
|
||||
border: 'none',
|
||||
header: 'underline',
|
||||
fontSize: 's',
|
||||
};
|
||||
const defaultCellActionsOptions: CellActionsOptions = {
|
||||
getCellActionsForColumn: () => [],
|
||||
disabledCellActions: [],
|
||||
};
|
||||
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
const DEFAULT_ACTIONS_COLUMN_WIDTH = 75;
|
||||
const stableMappedRowClasses: EuiDataGridStyle['rowClasses'] = {};
|
||||
|
||||
export const AlertsDataGrid = typedMemo(
|
||||
<AC extends AdditionalContext>(props: AlertsDataGridProps<AC>) => {
|
||||
const {
|
||||
ruleTypeIds,
|
||||
query,
|
||||
visibleColumns,
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
onChangeVisibleColumns,
|
||||
onColumnResize,
|
||||
showInspectButton = false,
|
||||
leadingControlColumns: additionalLeadingControlColumns,
|
||||
trailingControlColumns,
|
||||
onSortChange,
|
||||
sort: sortingFields,
|
||||
rowHeightsOptions,
|
||||
dynamicRowHeight,
|
||||
alertsQuerySnapshot,
|
||||
additionalToolbarControls,
|
||||
toolbarVisibility: toolbarVisibilityProp,
|
||||
shouldHighlightRow,
|
||||
renderContext,
|
||||
hideBulkActions,
|
||||
casesConfiguration,
|
||||
flyoutAlertIndex,
|
||||
setFlyoutAlertIndex,
|
||||
onPaginateFlyout,
|
||||
onChangePageSize,
|
||||
onChangePageIndex,
|
||||
actionsColumnWidth = DEFAULT_ACTIONS_COLUMN_WIDTH,
|
||||
getBulkActions,
|
||||
fieldsBrowserOptions,
|
||||
cellActionsOptions,
|
||||
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
||||
height,
|
||||
...euiDataGridProps
|
||||
} = props;
|
||||
const {
|
||||
isLoading,
|
||||
alerts,
|
||||
alertsCount,
|
||||
isLoadingAlerts,
|
||||
browserFields,
|
||||
renderActionsCell,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
refresh: refreshQueries,
|
||||
columns,
|
||||
dataGridRef,
|
||||
services: { http, notifications, application, cases: casesService, settings },
|
||||
} = renderContext;
|
||||
|
||||
const { colorMode } = useEuiTheme();
|
||||
const { sortingColumns, onSort } = useSorting(onSortChange, visibleColumns, sortingFields);
|
||||
const {
|
||||
isBulkActionsColumnActive,
|
||||
bulkActionsState,
|
||||
bulkActions,
|
||||
setIsBulkActionsLoading,
|
||||
clearSelection,
|
||||
} = useBulkActions({
|
||||
ruleTypeIds,
|
||||
query,
|
||||
alertsCount: alerts.length,
|
||||
casesConfig: casesConfiguration,
|
||||
getBulkActions,
|
||||
refresh: refreshQueries,
|
||||
hideBulkActions,
|
||||
http,
|
||||
notifications,
|
||||
application,
|
||||
casesService,
|
||||
});
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
refreshQueries();
|
||||
clearSelection();
|
||||
}, [clearSelection, refreshQueries]);
|
||||
|
||||
const columnIds = useMemo(() => columns.map((column) => column.id), [columns]);
|
||||
|
||||
const toolbarVisibility = useGetToolbarVisibility({
|
||||
bulkActions,
|
||||
alertsCount,
|
||||
rowSelection: bulkActionsState.rowSelection,
|
||||
alerts,
|
||||
isLoading,
|
||||
columnIds,
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
browserFields,
|
||||
additionalToolbarControls,
|
||||
setIsBulkActionsLoading,
|
||||
clearSelection,
|
||||
refresh,
|
||||
fieldsBrowserOptions,
|
||||
alertsQuerySnapshot,
|
||||
showInspectButton,
|
||||
toolbarVisibilityProp,
|
||||
settings,
|
||||
});
|
||||
|
||||
const leadingControlColumns: EuiDataGridControlColumn[] | undefined = useMemo(() => {
|
||||
const controlColumns = [
|
||||
...(additionalLeadingControlColumns ?? []),
|
||||
...(isBulkActionsColumnActive
|
||||
? [
|
||||
{
|
||||
id: 'bulkActions',
|
||||
width: 30,
|
||||
headerCellRender: BulkActionsHeader,
|
||||
rowCellRender: BulkActionsCell,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// If the user provided an actions cell renderer, add the actions column
|
||||
...(renderActionsCell
|
||||
? [
|
||||
{
|
||||
id: 'expandColumn',
|
||||
width: actionsColumnWidth,
|
||||
headerCellRender: ControlColumnHeaderCell,
|
||||
// Though untyped, rowCellRender's CellPropsWithContext contains the correct context
|
||||
rowCellRender:
|
||||
ActionsCellHost as unknown as EuiDataGridControlColumn['rowCellRender'],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
if (controlColumns.length) {
|
||||
return controlColumns;
|
||||
}
|
||||
}, [
|
||||
additionalLeadingControlColumns,
|
||||
isBulkActionsColumnActive,
|
||||
renderActionsCell,
|
||||
actionsColumnWidth,
|
||||
]);
|
||||
|
||||
const flyoutRowIndex = flyoutAlertIndex + pageIndex * pageSize;
|
||||
|
||||
// Row classes do not deal with visible row indices, so we need to handle page offset
|
||||
const activeRowClasses = useMemo<NonNullable<EuiDataGridStyle['rowClasses']>>(
|
||||
() => ({
|
||||
[flyoutRowIndex]: 'alertsTableActiveRow',
|
||||
}),
|
||||
[flyoutRowIndex]
|
||||
);
|
||||
|
||||
const handleFlyoutClose = useCallback(() => setFlyoutAlertIndex(-1), [setFlyoutAlertIndex]);
|
||||
|
||||
const dataGridPagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
onChangeItemsPerPage: onChangePageSize,
|
||||
onChangePage: onChangePageIndex,
|
||||
}),
|
||||
[onChangePageIndex, onChangePageSize, pageIndex, pageSize, pageSizeOptions]
|
||||
);
|
||||
|
||||
const { getCellActionsForColumn, visibleCellActions, disabledCellActions } =
|
||||
cellActionsOptions ?? defaultCellActionsOptions;
|
||||
|
||||
const columnsWithCellActions = useMemo(() => {
|
||||
if (getCellActionsForColumn) {
|
||||
return columns.map((col, idx) => ({
|
||||
...col,
|
||||
...(!(disabledCellActions ?? []).includes(col.id)
|
||||
? {
|
||||
cellActions: getCellActionsForColumn(col.id, idx) ?? [],
|
||||
visibleCellActions,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
return columns;
|
||||
}, [getCellActionsForColumn, columns, disabledCellActions, visibleCellActions]);
|
||||
|
||||
// Update highlighted rows when alerts or pagination changes
|
||||
const highlightedRowClasses = useMemo(() => {
|
||||
if (shouldHighlightRow) {
|
||||
const emptyShouldHighlightRow: EuiDataGridStyle['rowClasses'] = {};
|
||||
return alerts.reduce<NonNullable<EuiDataGridStyle['rowClasses']>>(
|
||||
(rowClasses, alert, index) => {
|
||||
if (shouldHighlightRow(alert)) {
|
||||
rowClasses[index + pageIndex * pageSize] = 'alertsTableHighlightedRow';
|
||||
}
|
||||
|
||||
return rowClasses;
|
||||
},
|
||||
emptyShouldHighlightRow
|
||||
);
|
||||
} else {
|
||||
return stableMappedRowClasses;
|
||||
}
|
||||
}, [shouldHighlightRow, alerts, pageIndex, pageSize]);
|
||||
|
||||
const mergedGridStyle = useMemo(() => {
|
||||
const propGridStyle: NonNullable<EuiDataGridStyle> = props.gridStyle ?? {};
|
||||
// Merges default row classes, custom ones and adds the active row class style
|
||||
return {
|
||||
...defaultGridStyle,
|
||||
...propGridStyle,
|
||||
rowClasses: {
|
||||
// We're spreading the highlighted row classes first, so that the active
|
||||
// row classed can override the highlighted row classes.
|
||||
...highlightedRowClasses,
|
||||
...activeRowClasses,
|
||||
},
|
||||
};
|
||||
}, [activeRowClasses, highlightedRowClasses, props.gridStyle]);
|
||||
|
||||
// Merges the default grid style with the grid style that comes in through props.
|
||||
const actualGridStyle = useMemo(() => {
|
||||
const propGridStyle: NonNullable<EuiDataGridStyle> = props.gridStyle ?? {};
|
||||
// If ANY additional rowClasses have been provided, we need to merge them with our internal ones
|
||||
if (propGridStyle.rowClasses) {
|
||||
// Get all row indices with a rowClass.
|
||||
const mergedKeys = [
|
||||
...Object.keys(mergedGridStyle.rowClasses || {}),
|
||||
...Object.keys(propGridStyle.rowClasses || {}),
|
||||
];
|
||||
// Deduplicate keys to avoid extra iterations
|
||||
const dedupedKeys = Array.from(new Set(mergedKeys));
|
||||
|
||||
// For each index, merge row classes
|
||||
mergedGridStyle.rowClasses = dedupedKeys.reduce<
|
||||
NonNullable<EuiDataGridStyle['rowClasses']>
|
||||
>((rowClasses, key) => {
|
||||
const intKey = parseInt(key, 10);
|
||||
// Use internal row classes over custom row classes.
|
||||
rowClasses[intKey] =
|
||||
mergedGridStyle.rowClasses?.[intKey] || propGridStyle.rowClasses?.[intKey] || '';
|
||||
return rowClasses;
|
||||
}, {});
|
||||
}
|
||||
return mergedGridStyle;
|
||||
}, [props.gridStyle, mergedGridStyle]);
|
||||
|
||||
const renderCustomGridBody = useCallback<NonNullable<EuiDataGridProps['renderCustomGridBody']>>(
|
||||
({ visibleColumns: _visibleColumns, Cell, headerRow, footerRow }) => (
|
||||
<>
|
||||
{headerRow}
|
||||
<NonVirtualizedGridBody
|
||||
alerts={alerts}
|
||||
visibleColumns={_visibleColumns}
|
||||
Cell={Cell}
|
||||
actualGridStyle={actualGridStyle}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
isLoading={isLoadingAlerts}
|
||||
stripes={props.gridStyle?.stripes}
|
||||
/>
|
||||
{footerRow}
|
||||
</>
|
||||
),
|
||||
[alerts, actualGridStyle, pageIndex, pageSize, isLoadingAlerts, props.gridStyle?.stripes]
|
||||
);
|
||||
|
||||
const sortProps = useMemo(() => {
|
||||
return { columns: sortingColumns, onSort };
|
||||
}, [sortingColumns, onSort]);
|
||||
|
||||
const columnVisibility = useMemo(() => {
|
||||
return { visibleColumns, setVisibleColumns: onChangeVisibleColumns };
|
||||
}, [visibleColumns, onChangeVisibleColumns]);
|
||||
|
||||
const rowStyles = useMemo(
|
||||
() => css`
|
||||
.alertsTableHighlightedRow {
|
||||
background-color: ${euiThemeVars.euiColorHighlight};
|
||||
}
|
||||
|
||||
.alertsTableActiveRow {
|
||||
background-color: ${colorMode === 'LIGHT'
|
||||
? tint(euiThemeVars.euiColorLightShade, 0.5)
|
||||
: euiThemeVars.euiColorLightShade};
|
||||
}
|
||||
`,
|
||||
[colorMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<InspectButtonContainer>
|
||||
<section style={{ width: '100%' }} data-test-subj={props['data-test-subj']}>
|
||||
<Suspense fallback={null}>
|
||||
{flyoutAlertIndex > -1 && (
|
||||
<AlertsFlyout<AC>
|
||||
{...renderContext}
|
||||
alert={alerts[flyoutAlertIndex]}
|
||||
alertsCount={alertsCount}
|
||||
onClose={handleFlyoutClose}
|
||||
flyoutIndex={flyoutAlertIndex + pageIndex * pageSize}
|
||||
onPaginate={onPaginateFlyout}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
{alertsCount > 0 && (
|
||||
<EuiDataGrid
|
||||
{...euiDataGridProps}
|
||||
// As per EUI docs, it is not recommended to switch between undefined and defined height.
|
||||
// If user changes height, it is better to unmount and mount the component.
|
||||
// Ref: https://eui.elastic.co/#/tabular-content/data-grid#virtualization
|
||||
key={height ? 'fixedHeight' : 'autoHeight'}
|
||||
ref={dataGridRef}
|
||||
css={rowStyles}
|
||||
aria-label="Alerts table"
|
||||
data-test-subj="alertsTable"
|
||||
height={height}
|
||||
columns={columnsWithCellActions}
|
||||
columnVisibility={columnVisibility}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
rowCount={alertsCount}
|
||||
renderCustomGridBody={dynamicRowHeight ? renderCustomGridBody : undefined}
|
||||
cellContext={renderContext}
|
||||
// Cast necessary because `cellContext` is untyped in EuiDataGrid
|
||||
renderCellValue={CellValueHost as RenderCellValue}
|
||||
renderCellPopover={CellPopoverHost}
|
||||
gridStyle={actualGridStyle}
|
||||
sorting={sortProps}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
pagination={dataGridPagination}
|
||||
rowHeightsOptions={rowHeightsOptions}
|
||||
onColumnResize={onColumnResize}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
(AlertsDataGrid as FC).displayName = 'AlertsDataGrid';
|
|
@ -1,54 +1,57 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { AlertsFlyout } from './alerts_flyout';
|
||||
import { Alert, AlertsField } from '../../../../types';
|
||||
import { AlertsField, FlyoutSectionRenderer } from '../types';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
|
||||
type FlyoutProps = ComponentProps<FlyoutSectionRenderer>;
|
||||
|
||||
const onClose = jest.fn();
|
||||
const onPaginate = jest.fn();
|
||||
const props = {
|
||||
const props = createPartialObjectMock<FlyoutProps>({
|
||||
alert: {
|
||||
[AlertsField.name]: ['one'],
|
||||
[AlertsField.reason]: ['two'],
|
||||
_id: '0123456789',
|
||||
_index: '.alerts-default',
|
||||
} as unknown as Alert,
|
||||
alertsTableConfiguration: {
|
||||
id: 'test',
|
||||
casesFeatureId: 'testCases',
|
||||
columns: [
|
||||
{
|
||||
id: AlertsField.name,
|
||||
displayAsText: 'Name',
|
||||
initialWidth: 150,
|
||||
},
|
||||
{
|
||||
id: AlertsField.reason,
|
||||
displayAsText: 'Reason',
|
||||
initialWidth: 250,
|
||||
},
|
||||
],
|
||||
useInternalFlyout: () => ({
|
||||
body: () => <h3>Internal flyout body</h3>,
|
||||
header: null,
|
||||
footer: () => null,
|
||||
}),
|
||||
getRenderCellValue: jest.fn().mockImplementation((rcvProps) => {
|
||||
return `${rcvProps.colIndex}:${rcvProps.rowIndex}`;
|
||||
}),
|
||||
[AlertsField.name]: ['one'],
|
||||
[AlertsField.reason]: ['two'],
|
||||
},
|
||||
tableId: 'test',
|
||||
columns: [
|
||||
{
|
||||
id: AlertsField.name,
|
||||
displayAsText: 'Name',
|
||||
initialWidth: 150,
|
||||
},
|
||||
{
|
||||
id: AlertsField.reason,
|
||||
displayAsText: 'Reason',
|
||||
initialWidth: 250,
|
||||
},
|
||||
],
|
||||
renderCellValue: jest.fn((rcvProps) => {
|
||||
return (
|
||||
<>
|
||||
`${rcvProps.colIndex}:${rcvProps.rowIndex}`
|
||||
</>
|
||||
);
|
||||
}),
|
||||
renderFlyoutBody: () => <h3 data-test-subj="test-flyout-body">Internal flyout body</h3>,
|
||||
flyoutIndex: 0,
|
||||
alertsCount: 4,
|
||||
isLoading: false,
|
||||
onClose,
|
||||
onPaginate,
|
||||
};
|
||||
});
|
||||
|
||||
describe('AlertsFlyout', () => {
|
||||
afterEach(() => {
|
||||
|
@ -61,25 +64,15 @@ describe('AlertsFlyout', () => {
|
|||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
expect(wrapper.find('h3').first().text()).toBe('Internal flyout body');
|
||||
expect(wrapper.find('[data-test-subj="test-flyout-body"]').first().text()).toBe(
|
||||
'Internal flyout body'
|
||||
);
|
||||
});
|
||||
|
||||
const base = {
|
||||
body: () => null,
|
||||
header: () => null,
|
||||
footer: () => null,
|
||||
};
|
||||
it(`should use header from useInternalFlyout configuration`, async () => {
|
||||
const customProps = {
|
||||
it(`should use header from the alerts table props`, async () => {
|
||||
const customProps: FlyoutProps = {
|
||||
...props,
|
||||
alertsTableConfiguration: {
|
||||
...props.alertsTableConfiguration,
|
||||
useInternalFlyout: () => ({
|
||||
...base,
|
||||
header: () => <h4>Header</h4>,
|
||||
footer: () => null,
|
||||
}),
|
||||
},
|
||||
renderFlyoutHeader: () => <h4>Header</h4>,
|
||||
};
|
||||
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
|
||||
await act(async () => {
|
||||
|
@ -89,16 +82,10 @@ describe('AlertsFlyout', () => {
|
|||
expect(wrapper.find('h4').first().text()).toBe('Header');
|
||||
});
|
||||
|
||||
it(`should use body from useInternalFlyout configuration`, async () => {
|
||||
const customProps = {
|
||||
it(`should use body the alerts table props`, async () => {
|
||||
const customProps: FlyoutProps = {
|
||||
...props,
|
||||
alertsTableConfiguration: {
|
||||
...props.alertsTableConfiguration,
|
||||
useInternalFlyout: () => ({
|
||||
...base,
|
||||
body: () => <h5>Body</h5>,
|
||||
}),
|
||||
},
|
||||
renderFlyoutBody: () => <h5>Body</h5>,
|
||||
};
|
||||
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
|
||||
await act(async () => {
|
||||
|
@ -108,16 +95,10 @@ describe('AlertsFlyout', () => {
|
|||
expect(wrapper.find('h5').first().text()).toBe('Body');
|
||||
});
|
||||
|
||||
it(`should use footer from useInternalFlyout configuration`, async () => {
|
||||
const customProps = {
|
||||
it(`should use footer from the alerts table props`, async () => {
|
||||
const customProps: FlyoutProps = {
|
||||
...props,
|
||||
alertsTableConfiguration: {
|
||||
...props.alertsTableConfiguration,
|
||||
useInternalFlyout: () => ({
|
||||
...base,
|
||||
footer: () => <h6>Footer</h6>,
|
||||
}),
|
||||
},
|
||||
renderFlyoutFooter: () => <h6>Footer</h6>,
|
||||
};
|
||||
const wrapper = mountWithIntl(<AlertsFlyout {...customProps} />);
|
||||
await act(async () => {
|
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
* 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 React, { Suspense, lazy, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
|
||||
import React, { Suspense, useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlyout,
|
||||
|
@ -15,9 +18,16 @@ import {
|
|||
EuiPagination,
|
||||
EuiProgress,
|
||||
} from '@elastic/eui';
|
||||
import type { Alert, AlertsTableConfigurationRegistry } from '../../../../types';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import { DefaultAlertsFlyoutBody, DefaultAlertsFlyoutHeader } from './default_alerts_flyout';
|
||||
import {
|
||||
AdditionalContext,
|
||||
FlyoutSectionProps,
|
||||
FlyoutSectionRenderer,
|
||||
RenderContext,
|
||||
} from '../types';
|
||||
|
||||
const AlertsFlyoutHeader = lazy(() => import('./alerts_flyout_header'));
|
||||
const PAGINATION_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.paginationLabel',
|
||||
{
|
||||
|
@ -25,93 +35,74 @@ const PAGINATION_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
function usePrevious(alert: Alert) {
|
||||
const ref = useRef<Alert | null>(null);
|
||||
useEffect(() => {
|
||||
if (alert) {
|
||||
ref.current = alert;
|
||||
}
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
interface AlertsFlyoutProps {
|
||||
export const AlertsFlyout = <AC extends AdditionalContext>({
|
||||
alert,
|
||||
...renderContext
|
||||
}: RenderContext<AC> & {
|
||||
alert: Alert;
|
||||
alertsTableConfiguration: AlertsTableConfigurationRegistry;
|
||||
flyoutIndex: number;
|
||||
alertsCount: number;
|
||||
isLoading: boolean;
|
||||
onClose: () => void;
|
||||
onPaginate: (pageIndex: number) => void;
|
||||
id?: string;
|
||||
}
|
||||
export const AlertsFlyout: React.FunctionComponent<AlertsFlyoutProps> = ({
|
||||
alert,
|
||||
alertsTableConfiguration,
|
||||
flyoutIndex,
|
||||
alertsCount,
|
||||
isLoading,
|
||||
onClose,
|
||||
onPaginate,
|
||||
id,
|
||||
}: AlertsFlyoutProps) => {
|
||||
}) => {
|
||||
const {
|
||||
header: Header,
|
||||
body: Body,
|
||||
footer: Footer,
|
||||
} = alertsTableConfiguration?.useInternalFlyout?.() ?? {
|
||||
header: AlertsFlyoutHeader,
|
||||
body: null,
|
||||
footer: null,
|
||||
};
|
||||
flyoutIndex,
|
||||
alertsCount,
|
||||
onClose,
|
||||
onPaginate,
|
||||
isLoading,
|
||||
renderFlyoutHeader: Header = DefaultAlertsFlyoutHeader,
|
||||
renderFlyoutBody: Body = DefaultAlertsFlyoutBody,
|
||||
renderFlyoutFooter,
|
||||
} = renderContext;
|
||||
const Footer: FlyoutSectionRenderer<AC> | undefined = renderFlyoutFooter;
|
||||
const prevAlert = usePrevious(alert);
|
||||
const passedProps = useMemo(
|
||||
() => ({
|
||||
alert: alert === undefined && prevAlert != null ? prevAlert : alert,
|
||||
id,
|
||||
isLoading,
|
||||
}),
|
||||
const props = useMemo(
|
||||
() =>
|
||||
({
|
||||
...renderContext,
|
||||
// Show the previous alert while loading the next one
|
||||
alert: alert === undefined && prevAlert != null ? prevAlert : alert,
|
||||
} as FlyoutSectionProps<AC>),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[alert, id, isLoading]
|
||||
);
|
||||
|
||||
const FlyoutBody = useCallback(
|
||||
() =>
|
||||
Body ? (
|
||||
<Suspense fallback={null}>
|
||||
<Body {...passedProps} />
|
||||
</Suspense>
|
||||
) : null,
|
||||
[Body, passedProps]
|
||||
);
|
||||
|
||||
const FlyoutFooter = useCallback(
|
||||
() =>
|
||||
Footer ? (
|
||||
<Suspense fallback={null}>
|
||||
<Footer {...passedProps} />
|
||||
</Suspense>
|
||||
) : null,
|
||||
[Footer, passedProps]
|
||||
[alert, renderContext]
|
||||
);
|
||||
|
||||
const FlyoutHeader = useCallback(
|
||||
() =>
|
||||
Header ? (
|
||||
<Suspense fallback={null}>
|
||||
<Header {...passedProps} />
|
||||
<Header<AC> {...props} />
|
||||
</Suspense>
|
||||
) : null,
|
||||
[Header, passedProps]
|
||||
[Header, props]
|
||||
);
|
||||
|
||||
const FlyoutBody = useCallback(
|
||||
() =>
|
||||
Body ? (
|
||||
<Suspense fallback={null}>
|
||||
<Body<AC> {...props} />
|
||||
</Suspense>
|
||||
) : null,
|
||||
[Body, props]
|
||||
);
|
||||
|
||||
const FlyoutFooter = useCallback(
|
||||
() =>
|
||||
Footer ? (
|
||||
<Suspense fallback={null}>
|
||||
<Footer {...props} />
|
||||
</Suspense>
|
||||
) : null,
|
||||
[Footer, props]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} size="m" data-test-subj="alertsFlyout" ownFocus={false}>
|
||||
{isLoading && <EuiProgress size="xs" color="accent" data-test-subj="alertsFlyoutLoading" />}
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<Suspense fallback={null}>
|
||||
<FlyoutHeader />
|
||||
</Suspense>
|
||||
<FlyoutHeader />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -131,3 +122,8 @@ export const AlertsFlyout: React.FunctionComponent<AlertsFlyoutProps> = ({
|
|||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
// Lazy loading helpers
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AlertsFlyout as default };
|
||||
export type AlertsFlyout = typeof AlertsFlyout;
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { AlertsQueryInspector } from './alerts_query_inspector';
|
||||
import { AlertsQueryInspectorModal } from './alerts_query_inspector_modal';
|
||||
|
||||
jest.mock('./alerts_query_inspector_modal');
|
||||
jest
|
||||
.mocked(AlertsQueryInspectorModal.type)
|
||||
.mockImplementation(() => <div data-test-subj="mocked-modal" />);
|
||||
|
||||
describe('AlertsQueryInspector', () => {
|
||||
const alertsQuerySnapshot = {
|
||||
request: [''],
|
||||
response: [''],
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('open Inspect Modal', async () => {
|
||||
render(
|
||||
<AlertsQueryInspector
|
||||
inspectTitle={'Inspect Title'}
|
||||
showInspectButton
|
||||
alertsQuerySnapshot={alertsQuerySnapshot}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(await screen.findByTestId('inspect-icon-button'));
|
||||
|
||||
expect(await screen.findByTestId('mocked-modal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -1,18 +1,20 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon } from '@elastic/eui';
|
||||
import React, { useState, memo, useCallback } from 'react';
|
||||
|
||||
import { EsQuerySnapshot } from '@kbn/alerts-ui-shared';
|
||||
import { EsQuerySnapshot } from '@kbn/alerting-types';
|
||||
import { HoverVisibilityContainer } from './hover_visibility_container';
|
||||
|
||||
import { ModalInspectQuery } from './modal';
|
||||
import * as i18n from './translations';
|
||||
import { AlertsQueryInspectorModal } from './alerts_query_inspector_modal';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export const BUTTON_CLASS = 'inspectButtonComponent';
|
||||
const VISIBILITY_CLASSES = [BUTTON_CLASS];
|
||||
|
@ -33,11 +35,14 @@ export const InspectButtonContainer: React.FC<InspectButtonContainerProps> = mem
|
|||
interface InspectButtonProps {
|
||||
onCloseInspect?: () => void;
|
||||
showInspectButton?: boolean;
|
||||
querySnapshot: EsQuerySnapshot;
|
||||
alertsQuerySnapshot: EsQuerySnapshot;
|
||||
inspectTitle: string;
|
||||
}
|
||||
|
||||
const InspectButtonComponent: React.FC<InspectButtonProps> = ({ querySnapshot, inspectTitle }) => {
|
||||
const AlertsQueryInspectorComponent: React.FC<InspectButtonProps> = ({
|
||||
alertsQuerySnapshot,
|
||||
inspectTitle,
|
||||
}) => {
|
||||
const [isShowingModal, setIsShowingModal] = useState(false);
|
||||
|
||||
const onOpenModal = useCallback(() => {
|
||||
|
@ -60,10 +65,10 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({ querySnapshot, i
|
|||
onClick={onOpenModal}
|
||||
/>
|
||||
{isShowingModal && (
|
||||
<ModalInspectQuery
|
||||
<AlertsQueryInspectorModal
|
||||
closeModal={onCloseModal}
|
||||
data-test-subj="inspect-modal"
|
||||
querySnapshot={querySnapshot}
|
||||
alertsQuerySnapshot={alertsQuerySnapshot}
|
||||
title={inspectTitle}
|
||||
/>
|
||||
)}
|
||||
|
@ -71,5 +76,6 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({ querySnapshot, i
|
|||
);
|
||||
};
|
||||
|
||||
InspectButtonComponent.displayName = 'InspectButtonComponent';
|
||||
export const InspectButton = React.memo(InspectButtonComponent);
|
||||
AlertsQueryInspectorComponent.displayName = 'AlertsQueryInspectorComponent';
|
||||
|
||||
export const AlertsQueryInspector = React.memo(AlertsQueryInspectorComponent);
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
@ -10,8 +12,8 @@ import { of } from 'rxjs';
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import type { ModalInspectProps } from './modal';
|
||||
import { ModalInspectQuery } from './modal';
|
||||
import type { ModalInspectProps } from './alerts_query_inspector_modal';
|
||||
import { AlertsQueryInspectorModal } from './alerts_query_inspector_modal';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
@ -32,12 +34,12 @@ const getRequest = (
|
|||
const response =
|
||||
'{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}';
|
||||
|
||||
describe('Modal Inspect', () => {
|
||||
describe('AlertsQueryInspectorModal', () => {
|
||||
const closeModal = jest.fn();
|
||||
const defaultProps: ModalInspectProps = {
|
||||
closeModal,
|
||||
title: 'Inspect',
|
||||
querySnapshot: {
|
||||
alertsQuerySnapshot: {
|
||||
request: [getRequest()],
|
||||
response: [response],
|
||||
},
|
||||
|
@ -46,7 +48,7 @@ describe('Modal Inspect', () => {
|
|||
const renderModalInspectQuery = () => {
|
||||
const theme = { theme$: of({ darkMode: false, name: 'amsterdam' }) };
|
||||
const userProfile = userProfileServiceMock.createStart();
|
||||
return render(<ModalInspectQuery {...defaultProps} />, {
|
||||
return render(<AlertsQueryInspectorModal {...defaultProps} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<KibanaThemeProvider theme={theme} userProfile={userProfile}>
|
||||
{children}
|
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
* 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 React, { type ReactNode } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCodeBlock,
|
||||
|
@ -19,17 +22,14 @@ import {
|
|||
EuiTabbedContent,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { EsQuerySnapshot } from '@kbn/alerts-ui-shared';
|
||||
import * as i18n from './translations';
|
||||
import type { EsQuerySnapshot } from '@kbn/alerting-types';
|
||||
import { css } from '@emotion/react';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export interface ModalInspectProps {
|
||||
closeModal: () => void;
|
||||
querySnapshot: EsQuerySnapshot;
|
||||
alertsQuerySnapshot: EsQuerySnapshot;
|
||||
title: string;
|
||||
}
|
||||
|
||||
|
@ -48,19 +48,6 @@ interface Response {
|
|||
aggregations: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const MyEuiModal = euiStyled(EuiModal)`
|
||||
width: min(768px, calc(100vw - 16px));
|
||||
min-height: 41vh;
|
||||
.euiModal__flex {
|
||||
width: 60vw;
|
||||
}
|
||||
.euiCodeBlock {
|
||||
height: auto !important;
|
||||
max-width: 718px;
|
||||
}
|
||||
`;
|
||||
|
||||
MyEuiModal.displayName = 'MyEuiModal';
|
||||
const parse = function <T>(str: string): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
|
@ -77,8 +64,12 @@ const stringify = (object: Request | Response): string => {
|
|||
}
|
||||
};
|
||||
|
||||
const ModalInspectQueryComponent = ({ closeModal, querySnapshot, title }: ModalInspectProps) => {
|
||||
const { request, response } = querySnapshot;
|
||||
const AlertsQueryInspectorModalComponent = ({
|
||||
closeModal,
|
||||
alertsQuerySnapshot,
|
||||
title,
|
||||
}: ModalInspectProps) => {
|
||||
const { request, response } = alertsQuerySnapshot;
|
||||
// using index 0 as there will be only one request and response for now
|
||||
const parsedRequest: Request = parse(request[0]);
|
||||
const parsedResponse: Response = parse(response[0]);
|
||||
|
@ -188,7 +179,21 @@ const ModalInspectQueryComponent = ({ closeModal, querySnapshot, title }: ModalI
|
|||
];
|
||||
|
||||
return (
|
||||
<MyEuiModal onClose={closeModal} data-test-subj="modal-inspect-euiModal">
|
||||
<EuiModal
|
||||
onClose={closeModal}
|
||||
data-test-subj="modal-inspect-euiModal"
|
||||
css={css`
|
||||
width: min(768px, calc(100vw - 16px));
|
||||
min-height: 41vh;
|
||||
.euiModal__flex {
|
||||
width: 60vw;
|
||||
}
|
||||
.euiCodeBlock {
|
||||
height: auto !important;
|
||||
max-width: 718px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.INSPECT} {title}
|
||||
|
@ -204,8 +209,8 @@ const ModalInspectQueryComponent = ({ closeModal, querySnapshot, title }: ModalI
|
|||
{i18n.CLOSE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</MyEuiModal>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalInspectQuery = React.memo(ModalInspectQueryComponent);
|
||||
export const AlertsQueryInspectorModal = React.memo(AlertsQueryInspectorModalComponent);
|
|
@ -0,0 +1,954 @@
|
|||
/*
|
||||
* 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 React, { FunctionComponent } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { get } from 'lodash';
|
||||
import { render, waitFor, screen, act } from '@testing-library/react';
|
||||
import { ALERT_CASE_IDS, ALERT_MAINTENANCE_WINDOW_IDS, ALERT_UUID } from '@kbn/rule-data-utils';
|
||||
import type { Alert, LegacyField } from '@kbn/alerting-types';
|
||||
import { settingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { fetchAlertsFields } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_fields';
|
||||
import { searchAlerts } from '@kbn/alerts-ui-shared/src/common/apis/search_alerts/search_alerts';
|
||||
import { testQueryClientConfig } from '@kbn/alerts-ui-shared/src/common/test_utils/test_query_client_config';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { getMutedAlertsInstancesByRule } from '@kbn/response-ops-alerts-apis/apis/get_muted_alerts_instances_by_rule';
|
||||
import { applicationServiceMock, notificationServiceMock } from '@kbn/core/public/mocks';
|
||||
import { afterAll } from '@elastic/synthetics';
|
||||
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
import {
|
||||
AlertsDataGridProps,
|
||||
AlertsTableProps,
|
||||
AdditionalContext,
|
||||
RenderContext,
|
||||
AlertsField,
|
||||
} from '../types';
|
||||
import { AlertsTable } from './alerts_table';
|
||||
import { AlertsDataGrid } from './alerts_data_grid';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { getCasesMock, createCasesServiceMock } from '../mocks/cases.mock';
|
||||
import { getMaintenanceWindowsMock } from '../mocks/maintenance_windows.mock';
|
||||
import { bulkGetCases } from '../apis/bulk_get_cases';
|
||||
import { bulkGetMaintenanceWindows } from '../apis/bulk_get_maintenance_windows';
|
||||
import { useLicense } from '../hooks/use_license';
|
||||
import { getJsDomPerformanceFix } from '../utils/test';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
|
||||
type BaseAlertsTableProps = AlertsTableProps;
|
||||
|
||||
// Search alerts mock
|
||||
jest.mock('@kbn/alerts-ui-shared/src/common/apis/search_alerts/search_alerts');
|
||||
const mockSearchAlerts = jest.mocked(searchAlerts);
|
||||
const columns = [
|
||||
{
|
||||
id: AlertsField.name,
|
||||
displayAsText: 'Name',
|
||||
},
|
||||
{
|
||||
id: AlertsField.reason,
|
||||
displayAsText: 'Reason',
|
||||
},
|
||||
{
|
||||
id: ALERT_CASE_IDS,
|
||||
displayAsText: 'Cases',
|
||||
},
|
||||
{
|
||||
id: ALERT_MAINTENANCE_WINDOW_IDS,
|
||||
displayAsText: 'Maintenance Windows',
|
||||
},
|
||||
];
|
||||
const alerts: Alert[] = [
|
||||
{
|
||||
_id: 'test-1',
|
||||
_index: 'alerts',
|
||||
[AlertsField.name]: ['one'],
|
||||
[AlertsField.reason]: ['two'],
|
||||
[AlertsField.uuid]: ['1047d115-670d-469e-af7a-86fdd2b2f814'],
|
||||
[ALERT_UUID]: ['alert-id-1'],
|
||||
[ALERT_CASE_IDS]: ['test-id'],
|
||||
[ALERT_MAINTENANCE_WINDOW_IDS]: ['test-mw-id-1'],
|
||||
},
|
||||
{
|
||||
_id: 'test-2',
|
||||
_index: 'alerts',
|
||||
[AlertsField.name]: ['three'],
|
||||
[AlertsField.reason]: ['four'],
|
||||
[AlertsField.uuid]: ['bf5f6d63-5afd-48e0-baf6-f28c2b68db46'],
|
||||
[ALERT_CASE_IDS]: ['test-id-2'],
|
||||
[ALERT_MAINTENANCE_WINDOW_IDS]: ['test-mw-id-2'],
|
||||
},
|
||||
{
|
||||
_id: 'test-3',
|
||||
_index: 'alerts',
|
||||
[AlertsField.name]: ['five'],
|
||||
[AlertsField.reason]: ['six'],
|
||||
[AlertsField.uuid]: ['1047d115-5afd-469e-baf6-f28c2b68db46'],
|
||||
[ALERT_CASE_IDS]: [],
|
||||
[ALERT_MAINTENANCE_WINDOW_IDS]: [],
|
||||
},
|
||||
];
|
||||
const oldAlertsData = [
|
||||
[
|
||||
{
|
||||
field: AlertsField.name,
|
||||
value: ['one'],
|
||||
},
|
||||
{
|
||||
field: AlertsField.reason,
|
||||
value: ['two'],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field: AlertsField.name,
|
||||
value: ['three'],
|
||||
},
|
||||
{
|
||||
field: AlertsField.reason,
|
||||
value: ['four'],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
field: AlertsField.name,
|
||||
value: ['five'],
|
||||
},
|
||||
{
|
||||
field: AlertsField.reason,
|
||||
value: ['six'],
|
||||
},
|
||||
],
|
||||
] as LegacyField[][];
|
||||
const ecsAlertsData = [
|
||||
[
|
||||
{
|
||||
'@timestamp': ['2023-01-28T10:48:49.559Z'],
|
||||
_id: 'SomeId',
|
||||
_index: 'SomeIndex',
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
name: ['one'],
|
||||
},
|
||||
reason: ['two'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
'@timestamp': ['2023-01-27T10:48:49.559Z'],
|
||||
_id: 'SomeId2',
|
||||
_index: 'SomeIndex',
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
name: ['three'],
|
||||
},
|
||||
reason: ['four'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
'@timestamp': ['2023-01-26T10:48:49.559Z'],
|
||||
_id: 'SomeId3',
|
||||
_index: 'SomeIndex',
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
name: ['five'],
|
||||
},
|
||||
reason: ['six'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
const mockSearchAlertsResponse: Awaited<ReturnType<typeof searchAlerts>> = {
|
||||
alerts,
|
||||
ecsAlertsData,
|
||||
oldAlertsData,
|
||||
total: alerts.length,
|
||||
querySnapshot: { request: [], response: [] },
|
||||
};
|
||||
|
||||
// Alerts fields mock
|
||||
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_alerts_fields');
|
||||
jest.mocked(fetchAlertsFields).mockResolvedValue({
|
||||
browserFields: {
|
||||
kibana: {
|
||||
fields: {
|
||||
[AlertsField.uuid]: {
|
||||
category: 'kibana',
|
||||
name: AlertsField.uuid,
|
||||
},
|
||||
[AlertsField.name]: {
|
||||
category: 'kibana',
|
||||
name: AlertsField.name,
|
||||
},
|
||||
[AlertsField.reason]: {
|
||||
category: 'kibana',
|
||||
name: AlertsField.reason,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
});
|
||||
|
||||
// Muted alerts mock
|
||||
jest.mock('@kbn/response-ops-alerts-apis/apis/get_muted_alerts_instances_by_rule');
|
||||
jest.mocked(getMutedAlertsInstancesByRule).mockResolvedValue({
|
||||
data: [],
|
||||
});
|
||||
|
||||
// Cases mock
|
||||
jest.mock('../apis/bulk_get_cases');
|
||||
const mockBulkGetCases = jest.mocked(bulkGetCases);
|
||||
const mockCases = getCasesMock();
|
||||
mockBulkGetCases.mockResolvedValue({ cases: mockCases, errors: [] });
|
||||
|
||||
// Maintenance windows mock
|
||||
jest.mock('../apis/bulk_get_maintenance_windows');
|
||||
jest.mock('../hooks/use_license');
|
||||
const mockBulkGetMaintenanceWindows = jest.mocked(bulkGetMaintenanceWindows);
|
||||
jest.mocked(useLicense).mockReturnValue({ isAtLeastPlatinum: () => true });
|
||||
const mockMaintenanceWindows = getMaintenanceWindowsMock();
|
||||
mockBulkGetMaintenanceWindows.mockResolvedValue({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
errors: [],
|
||||
});
|
||||
|
||||
// AlertsDataGrid mock
|
||||
jest.mock('./alerts_data_grid', () => ({
|
||||
AlertsDataGrid: jest.fn(),
|
||||
}));
|
||||
const mockAlertsDataGrid = jest.mocked(AlertsDataGrid);
|
||||
|
||||
const applicationMock = applicationServiceMock.createStartContract();
|
||||
const mockCurrentAppId$ = new BehaviorSubject<string>('testAppId');
|
||||
const mockCaseService = createCasesServiceMock();
|
||||
|
||||
const { fix, cleanup } = getJsDomPerformanceFix();
|
||||
|
||||
beforeAll(() => {
|
||||
fix();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient(testQueryClientConfig);
|
||||
const TestComponent: FunctionComponent<BaseAlertsTableProps> = (props) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IntlProvider locale="en">
|
||||
<AlertsTable {...props} />
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('AlertsTable', () => {
|
||||
// Storage mock
|
||||
const mockStorageGet = jest.fn();
|
||||
jest.mock('../utils/storage', () => ({
|
||||
Storage: jest.fn().mockReturnValue({
|
||||
get: mockStorageGet,
|
||||
set: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const tableProps: BaseAlertsTableProps = {
|
||||
id: 'test-alerts-table',
|
||||
ruleTypeIds: ['logs'],
|
||||
query: {},
|
||||
columns,
|
||||
initialPageSize: 10,
|
||||
renderActionsCell: ({ openAlertInFlyout }) => {
|
||||
return (
|
||||
<button
|
||||
data-test-subj="expandColumnCellOpenFlyoutButton-0"
|
||||
onClick={() => {
|
||||
openAlertInFlyout('alert-id-1');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderFlyoutBody: ({ alert }) => (
|
||||
<ul>
|
||||
{columns.map((column) => (
|
||||
<li data-test-subj={`alertsFlyout${column.displayAsText}`} key={column.id}>
|
||||
{get(alert as any, column.id, [])[0]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
services: {
|
||||
http: httpServiceMock.createStartContract(),
|
||||
application: {
|
||||
...applicationMock,
|
||||
getUrlForApp: jest.fn(() => ''),
|
||||
capabilities: {
|
||||
...applicationMock.capabilities,
|
||||
cases: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
},
|
||||
maintenanceWindow: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
currentAppId$: mockCurrentAppId$,
|
||||
},
|
||||
data: dataPluginMock.createStartContract(),
|
||||
fieldFormats: fieldFormatsMock,
|
||||
licensing: licensingMock.createStart(),
|
||||
notifications: notificationServiceMock.createStartContract(),
|
||||
settings: settingsServiceMock.createStartContract(),
|
||||
},
|
||||
};
|
||||
|
||||
let onChangePageIndex: AlertsDataGridProps['onChangePageIndex'];
|
||||
let refresh: RenderContext<AdditionalContext>['refresh'];
|
||||
|
||||
mockAlertsDataGrid.mockImplementation((props) => {
|
||||
const { AlertsDataGrid: ActualAlertsDataGrid } = jest.requireActual('./alerts_data_grid');
|
||||
onChangePageIndex = props.onChangePageIndex;
|
||||
refresh = props.renderContext.refresh;
|
||||
return <ActualAlertsDataGrid {...props} />;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSearchAlerts.mockResolvedValue(mockSearchAlertsResponse);
|
||||
});
|
||||
|
||||
describe('Cases', () => {
|
||||
const casesTableProps = {
|
||||
...tableProps,
|
||||
services: {
|
||||
...tableProps.services,
|
||||
cases: mockCaseService,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCaseService.helpers.canUseCases = jest.fn().mockReturnValue({ create: true, read: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockCaseService.ui.getCasesContext = jest.fn().mockImplementation(() => null);
|
||||
});
|
||||
|
||||
it('should show the cases column', async () => {
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
expect(await screen.findByText('Cases')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the cases titles correctly', async () => {
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
expect(await screen.findByText('Test case')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Test case 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the loading skeleton when fetching cases', async () => {
|
||||
mockBulkGetCases.mockResolvedValue({ cases: mockCases, errors: [] });
|
||||
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
expect((await screen.findAllByTestId('cases-cell-loading')).length).toBe(3);
|
||||
});
|
||||
|
||||
it('should pass the correct case ids to useBulkGetCases', async () => {
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetCases).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ ids: ['test-id', 'test-id-2'] }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('remove duplicated case ids', async () => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
...mockSearchAlertsResponse,
|
||||
alerts: [...mockSearchAlertsResponse.alerts, ...mockSearchAlertsResponse.alerts],
|
||||
});
|
||||
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetCases).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ ids: ['test-id', 'test-id-2'] }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('skips alerts with empty case ids', async () => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
...mockSearchAlertsResponse,
|
||||
alerts: [
|
||||
{
|
||||
...mockSearchAlertsResponse.alerts[0],
|
||||
'kibana.alert.case_ids': [],
|
||||
},
|
||||
mockSearchAlertsResponse.alerts[1],
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetCases).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ ids: ['test-id-2'] }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch cases if the user does not have permissions', async () => {
|
||||
mockCaseService.helpers.canUseCases = jest
|
||||
.fn()
|
||||
.mockReturnValue({ create: false, read: false });
|
||||
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetCases).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch cases if the column is not visible', async () => {
|
||||
mockCaseService.helpers.canUseCases = jest.fn().mockReturnValue({ create: true, read: true });
|
||||
|
||||
const props: BaseAlertsTableProps = {
|
||||
...casesTableProps,
|
||||
casesConfiguration: { featureId: 'test-feature-id', owner: ['cases'] },
|
||||
};
|
||||
|
||||
render(
|
||||
<TestComponent
|
||||
{...props}
|
||||
columns={[
|
||||
{
|
||||
id: AlertsField.name,
|
||||
displayAsText: 'Name',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetCases).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls canUseCases with an empty array if the case configuration is not defined', async () => {
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
expect(mockCaseService.helpers.canUseCases).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('calls canUseCases with the case owner if defined', async () => {
|
||||
const props: BaseAlertsTableProps = {
|
||||
...casesTableProps,
|
||||
casesConfiguration: { featureId: 'test-feature-id', owner: ['cases'] },
|
||||
};
|
||||
|
||||
render(<TestComponent {...props} />);
|
||||
expect(mockCaseService.helpers.canUseCases).toHaveBeenCalledWith(['cases']);
|
||||
});
|
||||
|
||||
it('should call the cases context with the correct props', async () => {
|
||||
const props: BaseAlertsTableProps = {
|
||||
...casesTableProps,
|
||||
casesConfiguration: { featureId: 'test-feature-id', owner: ['cases'] },
|
||||
};
|
||||
|
||||
const CasesContextMock = jest.fn().mockReturnValue(null);
|
||||
mockCaseService.ui.getCasesContext = jest.fn().mockReturnValue(CasesContextMock);
|
||||
|
||||
render(<TestComponent {...props} />);
|
||||
|
||||
expect(CasesContextMock).toHaveBeenCalledWith(
|
||||
{
|
||||
children: expect.anything(),
|
||||
owner: ['cases'],
|
||||
permissions: { create: true, read: true },
|
||||
features: { alerts: { sync: false } },
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the cases context with the empty owner if the case config is not defined', async () => {
|
||||
const CasesContextMock = jest.fn().mockReturnValue(null);
|
||||
mockCaseService.ui.getCasesContext = jest.fn().mockReturnValue(CasesContextMock);
|
||||
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
expect(CasesContextMock).toHaveBeenCalledWith(
|
||||
{
|
||||
children: expect.anything(),
|
||||
owner: [],
|
||||
permissions: { create: true, read: true },
|
||||
features: { alerts: { sync: false } },
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the cases context with correct permissions', async () => {
|
||||
const CasesContextMock = jest.fn().mockReturnValue(null);
|
||||
mockCaseService.ui.getCasesContext = jest.fn().mockReturnValue(CasesContextMock);
|
||||
mockCaseService.helpers.canUseCases = jest
|
||||
.fn()
|
||||
.mockReturnValue({ create: false, read: false });
|
||||
|
||||
render(<TestComponent {...casesTableProps} />);
|
||||
expect(CasesContextMock).toHaveBeenCalledWith(
|
||||
{
|
||||
children: expect.anything(),
|
||||
owner: [],
|
||||
permissions: { create: false, read: false },
|
||||
features: { alerts: { sync: false } },
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the cases context with sync alerts turned on if defined in the cases config', async () => {
|
||||
const props: BaseAlertsTableProps = {
|
||||
...casesTableProps,
|
||||
casesConfiguration: {
|
||||
featureId: 'test-feature-id',
|
||||
owner: ['cases'],
|
||||
syncAlerts: true,
|
||||
},
|
||||
};
|
||||
|
||||
const CasesContextMock = jest.fn().mockReturnValue(null);
|
||||
mockCaseService.ui.getCasesContext = jest.fn().mockReturnValue(CasesContextMock);
|
||||
|
||||
render(<TestComponent {...props} />);
|
||||
expect(CasesContextMock).toHaveBeenCalledWith(
|
||||
{
|
||||
children: expect.anything(),
|
||||
owner: ['cases'],
|
||||
permissions: { create: true, read: true },
|
||||
features: { alerts: { sync: true } },
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Maintenance windows', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should show maintenance windows column', async () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
expect(await screen.findByText('Maintenance Windows')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show maintenance windows titles correctly', async () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
expect(await screen.findByText('test-title')).toBeInTheDocument();
|
||||
expect(await screen.findByText('test-title-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass the correct maintenance window ids to useBulkGetMaintenanceWindows', async () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetMaintenanceWindows).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ids: ['test-mw-id-1', 'test-mw-id-2'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove duplicated maintenance window ids', async () => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
...mockSearchAlertsResponse,
|
||||
alerts: [...mockSearchAlertsResponse.alerts, ...mockSearchAlertsResponse.alerts],
|
||||
});
|
||||
|
||||
render(<TestComponent {...tableProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetMaintenanceWindows).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ids: ['test-mw-id-1', 'test-mw-id-2'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip alerts with empty maintenance window ids', async () => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
...mockSearchAlertsResponse,
|
||||
alerts: [
|
||||
{
|
||||
...mockSearchAlertsResponse.alerts[0],
|
||||
'kibana.alert.maintenance_window_ids': [],
|
||||
},
|
||||
mockSearchAlertsResponse.alerts[1],
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestComponent {...tableProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetMaintenanceWindows).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ids: ['test-mw-id-2'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show loading skeleton when fetching maintenance windows', async () => {
|
||||
mockBulkGetMaintenanceWindows.mockResolvedValue({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
errors: [],
|
||||
});
|
||||
|
||||
render(<TestComponent {...tableProps} />);
|
||||
expect((await screen.findAllByTestId('maintenance-window-cell-loading')).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not fetch maintenance windows if the user does not have permission', async () => {});
|
||||
|
||||
it('should not fetch maintenance windows if the column is not visible', async () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...tableProps}
|
||||
columns={[
|
||||
{
|
||||
id: AlertsField.name,
|
||||
displayAsText: 'Name',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockBulkGetMaintenanceWindows).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('flyout', () => {
|
||||
it('should show a flyout when selecting an alert', async () => {
|
||||
const wrapper = render(<TestComponent {...tableProps} />);
|
||||
await userEvent.click(wrapper.queryAllByTestId('expandColumnCellOpenFlyoutButton-0')[0]!);
|
||||
|
||||
const result = await wrapper.findAllByTestId('alertsFlyout');
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('one');
|
||||
expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('two');
|
||||
|
||||
// Should paginate too
|
||||
await userEvent.click(wrapper.queryAllByTestId('pagination-button-next')[0]);
|
||||
expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('three');
|
||||
expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('four');
|
||||
|
||||
await userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]);
|
||||
expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('one');
|
||||
expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('two');
|
||||
});
|
||||
|
||||
it('should refetch data if flyout pagination exceeds the current page', async () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...{
|
||||
...tableProps,
|
||||
initialPageSize: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(await screen.findByTestId('expandColumnCellOpenFlyoutButton-0'));
|
||||
const result = await screen.findAllByTestId('alertsFlyout');
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
mockSearchAlerts.mockClear();
|
||||
|
||||
await userEvent.click(await screen.findByTestId('pagination-button-next'));
|
||||
expect(mockSearchAlerts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pageIndex: 1,
|
||||
pageSize: 1,
|
||||
})
|
||||
);
|
||||
|
||||
mockSearchAlerts.mockClear();
|
||||
await userEvent.click(await screen.findByTestId('pagination-button-previous'));
|
||||
expect(mockSearchAlerts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pageIndex: 0,
|
||||
pageSize: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('Should be able to go back from last page to n - 1', async () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...{
|
||||
...tableProps,
|
||||
initialPageSize: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(
|
||||
(
|
||||
await screen.findAllByTestId('expandColumnCellOpenFlyoutButton-0')
|
||||
)[0]
|
||||
);
|
||||
const result = await screen.findAllByTestId('alertsFlyout');
|
||||
expect(result.length).toBe(1);
|
||||
|
||||
mockSearchAlerts.mockClear();
|
||||
|
||||
await userEvent.click(await screen.findByTestId('pagination-button-last'));
|
||||
expect(mockSearchAlerts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pageIndex: 1,
|
||||
pageSize: 2,
|
||||
})
|
||||
);
|
||||
|
||||
mockSearchAlerts.mockClear();
|
||||
await userEvent.click(await screen.findByTestId('pagination-button-previous'));
|
||||
expect(mockSearchAlerts).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pageIndex: 0,
|
||||
pageSize: 2,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('field browser', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockBulkGetCases.mockResolvedValue({ cases: [], errors: [] });
|
||||
mockBulkGetMaintenanceWindows.mockResolvedValue({
|
||||
maintenanceWindows: mockMaintenanceWindows,
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should show field browser', async () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
expect(await screen.findByTestId('show-field-browser')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should remove an already existing element when selected', async () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
|
||||
expect(screen.queryByTestId(`dataGridHeaderCell-${AlertsField.name}`)).not.toBe(null);
|
||||
await userEvent.click(await screen.findByTestId('show-field-browser'));
|
||||
const fieldCheckbox = screen.getByTestId(`field-${AlertsField.name}-checkbox`);
|
||||
await userEvent.click(fieldCheckbox);
|
||||
await userEvent.click(screen.getByTestId('close'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(`dataGridHeaderCell-${AlertsField.name}`)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should restore a default element that has been removed previously', async () => {
|
||||
mockStorageGet.mockClear();
|
||||
mockStorageGet.mockReturnValue({
|
||||
columns: [{ displayAsText: 'Reason', id: AlertsField.reason, schema: undefined }],
|
||||
sort: [
|
||||
{
|
||||
[AlertsField.reason]: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
],
|
||||
visibleColumns: [AlertsField.reason],
|
||||
});
|
||||
|
||||
render(<TestComponent {...tableProps} />);
|
||||
|
||||
expect(screen.queryByTestId(`dataGridHeaderCell-${AlertsField.name}`)).toBe(null);
|
||||
await userEvent.click(await screen.findByTestId('show-field-browser'));
|
||||
const fieldCheckbox = screen.getByTestId(`field-${AlertsField.name}-checkbox`);
|
||||
await userEvent.click(fieldCheckbox);
|
||||
await userEvent.click(screen.getByTestId('close'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId(`dataGridHeaderCell-${AlertsField.name}`)).not.toBe(null);
|
||||
const titles: string[] = [];
|
||||
screen
|
||||
.getByTestId('dataGridHeader')
|
||||
.querySelectorAll('.euiDataGridHeaderCell__content')
|
||||
.forEach((n) => titles.push(n?.getAttribute('title') ?? ''));
|
||||
expect(titles).toContain('Name');
|
||||
});
|
||||
});
|
||||
|
||||
it('should insert a new field as column when its not a default one', async () => {
|
||||
const { getByTestId, queryByTestId } = render(<TestComponent {...tableProps} />);
|
||||
|
||||
expect(queryByTestId(`dataGridHeaderCell-${AlertsField.uuid}`)).toBe(null);
|
||||
await userEvent.click(getByTestId('show-field-browser'));
|
||||
const fieldCheckbox = getByTestId(`field-${AlertsField.uuid}-checkbox`);
|
||||
await userEvent.click(fieldCheckbox);
|
||||
await userEvent.click(getByTestId('close'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId(`dataGridHeaderCell-${AlertsField.uuid}`)).not.toBe(null);
|
||||
expect(
|
||||
queryByTestId(`dataGridHeaderCell-${AlertsField.uuid}`)!
|
||||
.querySelector('.euiDataGridHeaderCell__content')!
|
||||
.getAttribute('title')
|
||||
).toBe(AlertsField.uuid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const testPersistentControls = () => {
|
||||
describe('persistent controls', () => {
|
||||
it('should show persistent controls if set', async () => {
|
||||
const props: BaseAlertsTableProps = {
|
||||
...tableProps,
|
||||
renderAdditionalToolbarControls: () => <span>This is a persistent control</span>,
|
||||
};
|
||||
render(<TestComponent {...props} />);
|
||||
expect(await screen.findByText('This is a persistent control')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
};
|
||||
testPersistentControls();
|
||||
|
||||
const testInspectButton = () => {
|
||||
describe('inspect button', () => {
|
||||
it('should hide the inspect button by default', () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
expect(screen.queryByTestId('inspect-icon-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the inspect button if the right prop is set', async () => {
|
||||
const props: BaseAlertsTableProps = {
|
||||
...tableProps,
|
||||
showInspectButton: true,
|
||||
};
|
||||
render(<TestComponent {...props} />);
|
||||
expect(await screen.findByTestId('inspect-icon-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
};
|
||||
testInspectButton();
|
||||
|
||||
describe('empty state', () => {
|
||||
beforeEach(() => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
alerts: [],
|
||||
oldAlertsData: [],
|
||||
ecsAlertsData: [],
|
||||
total: 0,
|
||||
querySnapshot: { request: [], response: [] },
|
||||
});
|
||||
});
|
||||
|
||||
it('should render an empty screen if there are no alerts', async () => {
|
||||
render(<TestComponent {...tableProps} />);
|
||||
expect(await screen.findByTestId('alertsTableEmptyState')).toBeTruthy();
|
||||
});
|
||||
|
||||
testInspectButton();
|
||||
|
||||
describe('when persistent controls are set', () => {
|
||||
testPersistentControls();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Client provided toolbar visibility options', () => {
|
||||
it('hide column order control', () => {
|
||||
const props: BaseAlertsTableProps = {
|
||||
...tableProps,
|
||||
toolbarVisibility: { showColumnSelector: false },
|
||||
};
|
||||
|
||||
render(<TestComponent {...props} />);
|
||||
|
||||
expect(screen.queryByTestId('dataGridColumnSelectorButton')).not.toBeInTheDocument();
|
||||
});
|
||||
it('hide sort Selection', () => {
|
||||
const customTableProps: BaseAlertsTableProps = {
|
||||
...tableProps,
|
||||
toolbarVisibility: { showSortSelector: false },
|
||||
};
|
||||
|
||||
render(<TestComponent {...customTableProps} />);
|
||||
|
||||
expect(screen.queryByTestId('dataGridColumnSortingButton')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('resets the page index when any query parameter changes', () => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
...mockSearchAlertsResponse,
|
||||
alerts: Array.from({ length: 100 }).map((_, i) => ({
|
||||
_id: `${i}`,
|
||||
_index: 'alerts',
|
||||
[AlertsField.uuid]: [`alert-${i}`],
|
||||
})),
|
||||
});
|
||||
const { rerender } = render(<TestComponent {...tableProps} />);
|
||||
act(() => {
|
||||
onChangePageIndex(1);
|
||||
});
|
||||
rerender(
|
||||
<TestComponent
|
||||
{...tableProps}
|
||||
query={{ bool: { filter: [{ term: { 'kibana.alert.rule.name': 'test' } }] } }}
|
||||
/>
|
||||
);
|
||||
expect(mockSearchAlerts).toHaveBeenLastCalledWith(expect.objectContaining({ pageIndex: 0 }));
|
||||
});
|
||||
|
||||
it('resets the page index when refetching alerts', () => {
|
||||
mockSearchAlerts.mockResolvedValue({
|
||||
...mockSearchAlertsResponse,
|
||||
alerts: Array.from({ length: 100 }).map((_, i) => ({
|
||||
_id: `${i}`,
|
||||
_index: 'alerts',
|
||||
[AlertsField.uuid]: [`alert-${i}`],
|
||||
})),
|
||||
});
|
||||
render(<TestComponent {...tableProps} />);
|
||||
act(() => {
|
||||
onChangePageIndex(1);
|
||||
});
|
||||
act(() => {
|
||||
refresh();
|
||||
});
|
||||
expect(mockSearchAlerts).toHaveBeenLastCalledWith(expect.objectContaining({ pageIndex: 0 }));
|
||||
});
|
||||
});
|
||||
});
|
616
packages/response-ops/alerts_table/components/alerts_table.tsx
Normal file
616
packages/response-ops/alerts_table/components/alerts_table.tsx
Normal file
|
@ -0,0 +1,616 @@
|
|||
/*
|
||||
* 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 React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
Ref,
|
||||
memo,
|
||||
FC,
|
||||
} from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
EuiDataGridColumn,
|
||||
EuiProgress,
|
||||
EuiDataGridSorting,
|
||||
EuiDataGridControlColumn,
|
||||
EuiDataGridRefProps,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ALERT_CASE_IDS,
|
||||
ALERT_MAINTENANCE_WINDOW_IDS,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_UUID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import type { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
|
||||
import type { SortCombinations } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { QueryClientProvider, useQueryClient } from '@tanstack/react-query';
|
||||
import { useSearchAlertsQuery } from '@kbn/alerts-ui-shared/src/common/hooks/use_search_alerts_query';
|
||||
import { DEFAULT_ALERTS_PAGE_SIZE } from '@kbn/alerts-ui-shared/src/common/constants';
|
||||
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { Alert } from '@kbn/alerting-types';
|
||||
import { useGetMutedAlertsQuery } from '@kbn/response-ops-alerts-apis/hooks/use_get_muted_alerts_query';
|
||||
import { queryKeys as alertsQueryKeys } from '@kbn/response-ops-alerts-apis/constants';
|
||||
import { ErrorFallback } from './error_fallback';
|
||||
import { defaultAlertsTableColumns } from '../configuration';
|
||||
import { Storage } from '../utils/storage';
|
||||
import { queryKeys } from '../constants';
|
||||
import { AlertsDataGrid } from './alerts_data_grid';
|
||||
import { EmptyState } from './empty_state';
|
||||
import { RenderContext, RowSelectionState } from '../types';
|
||||
import {
|
||||
AdditionalContext,
|
||||
AlertsDataGridProps,
|
||||
AlertsTableImperativeApi,
|
||||
AlertsTableProps,
|
||||
} from '../types';
|
||||
import { bulkActionsReducer } from '../reducers/bulk_actions_reducer';
|
||||
import { useColumns } from '../hooks/use_columns';
|
||||
import { InspectButtonContainer } from './alerts_query_inspector';
|
||||
import { alertsTableQueryClient } from '../query_client';
|
||||
import { useBulkGetCasesQuery } from '../hooks/use_bulk_get_cases';
|
||||
import { useBulkGetMaintenanceWindowsQuery } from '../hooks/use_bulk_get_maintenance_windows';
|
||||
import { AlertsTableContextProvider } from '../contexts/alerts_table_context';
|
||||
import { ErrorBoundary } from './error_boundary';
|
||||
import { usePagination } from '../hooks/use_pagination';
|
||||
import { typedForwardRef } from '../utils/react';
|
||||
|
||||
export interface AlertsTablePersistedConfiguration {
|
||||
columns: EuiDataGridColumn[];
|
||||
visibleColumns?: string[];
|
||||
sort: SortCombinations[];
|
||||
}
|
||||
|
||||
type AlertWithCaseIds = Alert & Required<Pick<Alert, typeof ALERT_CASE_IDS>>;
|
||||
type AlertWithMaintenanceWindowIds = Alert &
|
||||
Required<Pick<Alert, typeof ALERT_MAINTENANCE_WINDOW_IDS>>;
|
||||
|
||||
const getCaseIdsFromAlerts = (alerts: Alert[]) => [
|
||||
...new Set(
|
||||
alerts
|
||||
.filter((alert): alert is AlertWithCaseIds => {
|
||||
const caseIds = alert[ALERT_CASE_IDS];
|
||||
return caseIds != null && caseIds.length > 0;
|
||||
})
|
||||
.map((alert) => alert[ALERT_CASE_IDS] as string[])
|
||||
.flat()
|
||||
),
|
||||
];
|
||||
|
||||
const getRuleIdsFromAlerts = (alerts: Alert[]) => [
|
||||
...new Set(alerts.map((a) => a[ALERT_RULE_UUID]![0] as string)),
|
||||
];
|
||||
|
||||
const getMaintenanceWindowIdsFromAlerts = (alerts: Alert[]) => [
|
||||
...new Set(
|
||||
alerts
|
||||
.filter((alert): alert is AlertWithMaintenanceWindowIds => {
|
||||
const maintenanceWindowIds = alert[ALERT_MAINTENANCE_WINDOW_IDS];
|
||||
return maintenanceWindowIds != null && maintenanceWindowIds.length > 0;
|
||||
})
|
||||
.map((alert) => alert[ALERT_MAINTENANCE_WINDOW_IDS] as string[])
|
||||
.flat()
|
||||
),
|
||||
];
|
||||
|
||||
const isCasesColumnEnabled = (columns: EuiDataGridColumn[]): boolean =>
|
||||
columns.some(({ id }) => id === ALERT_CASE_IDS);
|
||||
|
||||
const isMaintenanceWindowColumnEnabled = (columns: EuiDataGridColumn[]): boolean =>
|
||||
columns.some(({ id }) => id === ALERT_MAINTENANCE_WINDOW_IDS);
|
||||
|
||||
const emptyRowSelection = new Map<number, RowSelectionState>();
|
||||
|
||||
const initialBulkActionsState = {
|
||||
rowSelection: emptyRowSelection,
|
||||
isAllSelected: false,
|
||||
areAllVisibleRowsSelected: false,
|
||||
rowCount: 0,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
/**
|
||||
* An `EuiDataGrid` abstraction to render alert documents
|
||||
*
|
||||
* It manages the paginated and cached fetching of alerts based on the
|
||||
* provided `ruleTypeIds` and `consumers` (the final query can be refined
|
||||
* through the `query` and `initialSort` props). The `id` prop is required in order
|
||||
* to persist the table state in `localStorage`
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <AlertsTable
|
||||
* id="my-alerts-table"
|
||||
* ruleTypeIds={ruleTypeIds}
|
||||
* consumers={consumers}
|
||||
* query={esQuery}
|
||||
* initialSort={defaultAlertsTableSort}
|
||||
* renderCellValue={CellValue}
|
||||
* renderActionsCell={ActionsCell}
|
||||
* services={{ ... }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const AlertsTable = memo(
|
||||
forwardRef((props, ref) => {
|
||||
return (
|
||||
<QueryClientProvider client={alertsTableQueryClient} context={AlertsQueryContext}>
|
||||
<ErrorBoundary fallback={ErrorFallback}>
|
||||
<AlertsTableContent {...props} ref={ref} />
|
||||
</ErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
})
|
||||
// Type cast to avoid losing the generic type
|
||||
) as typeof AlertsTableContent;
|
||||
|
||||
(AlertsTable as FC).displayName = 'AlertsTable';
|
||||
|
||||
const DEFAULT_LEADING_CONTROL_COLUMNS: EuiDataGridControlColumn[] = [];
|
||||
const DEFAULT_SORT: SortCombinations[] = [];
|
||||
const DEFAULT_COLUMNS: EuiDataGridColumn[] = [];
|
||||
|
||||
const AlertsTableContent = typedForwardRef(
|
||||
<AC extends AdditionalContext>(
|
||||
{
|
||||
id,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
query,
|
||||
initialSort = DEFAULT_SORT,
|
||||
initialPageSize = DEFAULT_ALERTS_PAGE_SIZE,
|
||||
leadingControlColumns = DEFAULT_LEADING_CONTROL_COLUMNS,
|
||||
trailingControlColumns,
|
||||
rowHeightsOptions,
|
||||
columns: initialColumns = defaultAlertsTableColumns,
|
||||
gridStyle,
|
||||
browserFields: propBrowserFields,
|
||||
onUpdate,
|
||||
onLoaded,
|
||||
runtimeMappings,
|
||||
showAlertStatusWithFlapping,
|
||||
toolbarVisibility,
|
||||
shouldHighlightRow,
|
||||
dynamicRowHeight = false,
|
||||
emptyStateHeight,
|
||||
additionalContext,
|
||||
renderCellValue,
|
||||
renderCellPopover,
|
||||
renderActionsCell,
|
||||
renderFlyoutHeader,
|
||||
renderFlyoutBody,
|
||||
renderFlyoutFooter,
|
||||
renderAdditionalToolbarControls: AdditionalToolbarControlsComponent,
|
||||
lastReloadRequestTime,
|
||||
services,
|
||||
...publicDataGridProps
|
||||
}: AlertsTableProps<AC>,
|
||||
ref: Ref<AlertsTableImperativeApi>
|
||||
) => {
|
||||
// Memoized so that consumers can pass an inline object without causing re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const memoizedServices = useMemo(() => services, Object.values(services));
|
||||
const { casesConfiguration, showInspectButton } = publicDataGridProps;
|
||||
const { data, cases: casesService, http, notifications, application, licensing } = services;
|
||||
const queryClient = useQueryClient({ context: AlertsQueryContext });
|
||||
const storage = useRef(new Storage(window.localStorage));
|
||||
const dataGridRef = useRef<EuiDataGridRefProps>(null);
|
||||
const localStorageAlertsTableConfig = storage.current.get(
|
||||
id
|
||||
) as Partial<AlertsTablePersistedConfiguration>;
|
||||
|
||||
const columnsLocal = useMemo(
|
||||
() =>
|
||||
!isEmpty(localStorageAlertsTableConfig?.columns)
|
||||
? localStorageAlertsTableConfig!.columns!
|
||||
: !isEmpty(initialColumns)
|
||||
? initialColumns!
|
||||
: [],
|
||||
[initialColumns, localStorageAlertsTableConfig]
|
||||
);
|
||||
|
||||
const getStorageConfig = useCallback(
|
||||
() => ({
|
||||
columns: columnsLocal,
|
||||
sort: !isEmpty(localStorageAlertsTableConfig?.sort)
|
||||
? localStorageAlertsTableConfig!.sort!
|
||||
: initialSort ?? [],
|
||||
visibleColumns: !isEmpty(localStorageAlertsTableConfig?.visibleColumns)
|
||||
? localStorageAlertsTableConfig!.visibleColumns!
|
||||
: columnsLocal.map((c) => c.id),
|
||||
}),
|
||||
[columnsLocal, localStorageAlertsTableConfig, initialSort]
|
||||
);
|
||||
const storageAlertsTable = useRef<AlertsTablePersistedConfiguration>(getStorageConfig());
|
||||
|
||||
storageAlertsTable.current = getStorageConfig();
|
||||
|
||||
const [sort, setSort] = useState<SortCombinations[]>(storageAlertsTable.current.sort);
|
||||
|
||||
const onPageChange = useCallback((pagination: RuleRegistrySearchRequestPagination) => {
|
||||
setQueryParams((prevQueryParams) => ({
|
||||
...prevQueryParams,
|
||||
pageSize: pagination.pageSize,
|
||||
pageIndex: pagination.pageIndex,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const {
|
||||
columns,
|
||||
browserFields,
|
||||
isBrowserFieldDataLoading,
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
visibleColumns,
|
||||
onChangeVisibleColumns,
|
||||
onColumnResize,
|
||||
fields,
|
||||
} = useColumns({
|
||||
ruleTypeIds,
|
||||
storageAlertsTable,
|
||||
storage,
|
||||
id,
|
||||
defaultColumns: initialColumns ?? DEFAULT_COLUMNS,
|
||||
alertsFields: propBrowserFields,
|
||||
http,
|
||||
});
|
||||
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query,
|
||||
sort,
|
||||
runtimeMappings,
|
||||
pageIndex: 0,
|
||||
pageSize: initialPageSize,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setQueryParams(({ pageIndex: oldPageIndex, pageSize: oldPageSize, ...prevQueryParams }) => ({
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query,
|
||||
sort,
|
||||
runtimeMappings,
|
||||
// Go back to the first page if the query changes
|
||||
pageIndex: !deepEqual(prevQueryParams, {
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query,
|
||||
sort,
|
||||
runtimeMappings,
|
||||
})
|
||||
? 0
|
||||
: oldPageIndex,
|
||||
pageSize: oldPageSize,
|
||||
}));
|
||||
}, [ruleTypeIds, fields, query, runtimeMappings, sort, consumers]);
|
||||
|
||||
const {
|
||||
data: alertsData,
|
||||
refetch: refetchAlerts,
|
||||
isSuccess,
|
||||
isFetching: isLoadingAlerts,
|
||||
} = useSearchAlertsQuery({
|
||||
data,
|
||||
...queryParams,
|
||||
});
|
||||
const {
|
||||
alerts = [],
|
||||
oldAlertsData = [],
|
||||
ecsAlertsData = [],
|
||||
total: alertsCount = -1,
|
||||
querySnapshot: alertsQuerySnapshot,
|
||||
} = alertsData ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
if (onLoaded && !isLoadingAlerts && isSuccess) {
|
||||
onLoaded(alerts, columns);
|
||||
}
|
||||
}, [alerts, columns, isLoadingAlerts, isSuccess, onLoaded]);
|
||||
|
||||
const ruleIds = useMemo(() => getRuleIdsFromAlerts(alerts), [alerts]);
|
||||
const mutedAlertsQuery = useGetMutedAlertsQuery({
|
||||
ruleIds,
|
||||
http,
|
||||
notifications,
|
||||
});
|
||||
|
||||
const caseIds = useMemo(() => getCaseIdsFromAlerts(alerts), [alerts]);
|
||||
const casesPermissions = useMemo(() => {
|
||||
return casesService?.helpers.canUseCases(casesConfiguration?.owner ?? []);
|
||||
}, [casesConfiguration?.owner, casesService?.helpers]);
|
||||
const casesQuery = useBulkGetCasesQuery(
|
||||
{ caseIds, http, notifications },
|
||||
{
|
||||
enabled: isCasesColumnEnabled(columns) && !!casesPermissions?.read,
|
||||
}
|
||||
);
|
||||
|
||||
const maintenanceWindowIds = useMemo(() => getMaintenanceWindowIdsFromAlerts(alerts), [alerts]);
|
||||
const maintenanceWindowsQuery = useBulkGetMaintenanceWindowsQuery(
|
||||
{ ids: maintenanceWindowIds, http, application, notifications, licensing },
|
||||
{ enabled: isMaintenanceWindowColumnEnabled(columns), context: AlertsQueryContext }
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (queryParams.pageIndex !== 0) {
|
||||
// Refetch from the first page
|
||||
setQueryParams((prevQueryParams) => ({ ...prevQueryParams, pageIndex: 0 }));
|
||||
} else {
|
||||
refetchAlerts();
|
||||
}
|
||||
queryClient.invalidateQueries(queryKeys.casesBulkGet(caseIds));
|
||||
queryClient.invalidateQueries(alertsQueryKeys.mutedAlerts(ruleIds));
|
||||
queryClient.invalidateQueries(queryKeys.maintenanceWindowsBulkGet(maintenanceWindowIds));
|
||||
}, [caseIds, maintenanceWindowIds, queryClient, queryParams.pageIndex, refetchAlerts, ruleIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastReloadRequestTime) {
|
||||
refresh();
|
||||
}
|
||||
}, [lastReloadRequestTime, refresh]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh,
|
||||
toggleColumn: onToggleColumn,
|
||||
}));
|
||||
|
||||
const [bulkActionsState, dispatchBulkAction] = useReducer(
|
||||
bulkActionsReducer,
|
||||
initialBulkActionsState
|
||||
);
|
||||
|
||||
const bulkActionsStore = useMemo(
|
||||
() =>
|
||||
[bulkActionsState, dispatchBulkAction] as [
|
||||
typeof bulkActionsState,
|
||||
typeof dispatchBulkAction
|
||||
],
|
||||
[bulkActionsState, dispatchBulkAction]
|
||||
);
|
||||
|
||||
const onSortChange = useCallback(
|
||||
(_sort: EuiDataGridSorting['columns']) => {
|
||||
const newSort = _sort.map((sortItem) => {
|
||||
return {
|
||||
[sortItem.id]: {
|
||||
order: sortItem.direction,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
storageAlertsTable.current = {
|
||||
...storageAlertsTable.current,
|
||||
sort: newSort,
|
||||
};
|
||||
storage.current.set(id, storageAlertsTable.current);
|
||||
setSort(newSort);
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
||||
const CasesContext = useMemo(() => {
|
||||
return casesService?.ui.getCasesContext();
|
||||
}, [casesService?.ui]);
|
||||
|
||||
const isCasesContextAvailable = casesService && CasesContext;
|
||||
|
||||
const {
|
||||
pagination,
|
||||
onChangePageSize,
|
||||
onChangePageIndex,
|
||||
onPaginateFlyout,
|
||||
flyoutAlertIndex,
|
||||
setFlyoutAlertIndex,
|
||||
} = usePagination({
|
||||
bulkActionsStore,
|
||||
onPageChange,
|
||||
pageIndex: queryParams.pageIndex,
|
||||
pageSize: queryParams.pageSize,
|
||||
});
|
||||
|
||||
// TODO when every solution is using this table, we will be able to simplify it by just passing the alert index
|
||||
const openAlertInFlyout = useCallback(
|
||||
(alertId: string) => {
|
||||
const idx = alerts.findIndex((a) => (a as any)[ALERT_UUID].includes(alertId));
|
||||
setFlyoutAlertIndex(idx);
|
||||
},
|
||||
[alerts, setFlyoutAlertIndex]
|
||||
);
|
||||
|
||||
const renderContext = useMemo(
|
||||
() =>
|
||||
({
|
||||
...additionalContext,
|
||||
columns,
|
||||
tableId: id,
|
||||
dataGridRef,
|
||||
refresh,
|
||||
isLoading:
|
||||
isLoadingAlerts ||
|
||||
casesQuery.isFetching ||
|
||||
maintenanceWindowsQuery.isFetching ||
|
||||
mutedAlertsQuery.isFetching,
|
||||
isLoadingAlerts,
|
||||
alerts,
|
||||
alertsCount,
|
||||
// TODO deprecate
|
||||
ecsAlertsData,
|
||||
oldAlertsData,
|
||||
browserFields,
|
||||
isLoadingCases: casesQuery.isFetching,
|
||||
cases: casesQuery.data,
|
||||
isLoadingMaintenanceWindows: maintenanceWindowsQuery.isFetching,
|
||||
maintenanceWindows: maintenanceWindowsQuery.data,
|
||||
isLoadingMutedAlerts: mutedAlertsQuery.isFetching,
|
||||
mutedAlerts: mutedAlertsQuery.data,
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
showAlertStatusWithFlapping,
|
||||
openAlertInFlyout,
|
||||
bulkActionsStore,
|
||||
renderCellValue,
|
||||
renderCellPopover,
|
||||
renderActionsCell,
|
||||
renderFlyoutHeader,
|
||||
renderFlyoutBody,
|
||||
renderFlyoutFooter,
|
||||
services: memoizedServices,
|
||||
} as RenderContext<AC>),
|
||||
[
|
||||
additionalContext,
|
||||
columns,
|
||||
id,
|
||||
refresh,
|
||||
isLoadingAlerts,
|
||||
casesQuery.isFetching,
|
||||
casesQuery.data,
|
||||
maintenanceWindowsQuery.isFetching,
|
||||
maintenanceWindowsQuery.data,
|
||||
mutedAlertsQuery.isFetching,
|
||||
mutedAlertsQuery.data,
|
||||
alerts,
|
||||
alertsCount,
|
||||
ecsAlertsData,
|
||||
oldAlertsData,
|
||||
browserFields,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
showAlertStatusWithFlapping,
|
||||
openAlertInFlyout,
|
||||
bulkActionsStore,
|
||||
renderCellValue,
|
||||
renderCellPopover,
|
||||
renderActionsCell,
|
||||
renderFlyoutHeader,
|
||||
renderFlyoutBody,
|
||||
renderFlyoutFooter,
|
||||
memoizedServices,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (onUpdate) {
|
||||
onUpdate(renderContext);
|
||||
}
|
||||
}, [onUpdate, renderContext]);
|
||||
|
||||
const additionalToolbarControls = useMemo(
|
||||
() =>
|
||||
AdditionalToolbarControlsComponent ? (
|
||||
<AdditionalToolbarControlsComponent {...renderContext} />
|
||||
) : undefined,
|
||||
[AdditionalToolbarControlsComponent, renderContext]
|
||||
);
|
||||
|
||||
const dataGridProps: AlertsDataGridProps<AC> = useMemo(
|
||||
() => ({
|
||||
...publicDataGridProps,
|
||||
renderContext,
|
||||
additionalToolbarControls,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
visibleColumns,
|
||||
'data-test-subj': 'internalAlertsState',
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
onChangeVisibleColumns,
|
||||
onColumnResize,
|
||||
query,
|
||||
rowHeightsOptions,
|
||||
gridStyle,
|
||||
toolbarVisibility,
|
||||
shouldHighlightRow,
|
||||
dynamicRowHeight,
|
||||
ruleTypeIds,
|
||||
alertsQuerySnapshot,
|
||||
onChangePageIndex,
|
||||
onChangePageSize,
|
||||
onPaginateFlyout,
|
||||
flyoutAlertIndex,
|
||||
setFlyoutAlertIndex,
|
||||
sort,
|
||||
onSortChange,
|
||||
}),
|
||||
[
|
||||
publicDataGridProps,
|
||||
renderContext,
|
||||
additionalToolbarControls,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
visibleColumns,
|
||||
onToggleColumn,
|
||||
onResetColumns,
|
||||
onChangeVisibleColumns,
|
||||
onColumnResize,
|
||||
query,
|
||||
rowHeightsOptions,
|
||||
gridStyle,
|
||||
toolbarVisibility,
|
||||
shouldHighlightRow,
|
||||
dynamicRowHeight,
|
||||
ruleTypeIds,
|
||||
alertsQuerySnapshot,
|
||||
onChangePageIndex,
|
||||
onChangePageSize,
|
||||
onPaginateFlyout,
|
||||
flyoutAlertIndex,
|
||||
setFlyoutAlertIndex,
|
||||
sort,
|
||||
onSortChange,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertsTableContextProvider value={renderContext}>
|
||||
{!isLoadingAlerts && alertsCount <= 0 && (
|
||||
<InspectButtonContainer>
|
||||
<EmptyState
|
||||
additionalToolbarControls={additionalToolbarControls}
|
||||
alertsQuerySnapshot={alertsQuerySnapshot}
|
||||
showInspectButton={showInspectButton}
|
||||
height={emptyStateHeight}
|
||||
/>
|
||||
</InspectButtonContainer>
|
||||
)}
|
||||
{(isLoadingAlerts || isBrowserFieldDataLoading) && (
|
||||
<EuiProgress size="xs" color="accent" data-test-subj="internalAlertsPageLoading" />
|
||||
)}
|
||||
{alertsCount > 0 &&
|
||||
(isCasesContextAvailable ? (
|
||||
<CasesContext
|
||||
owner={casesConfiguration?.owner ?? []}
|
||||
permissions={casesPermissions}
|
||||
features={{ alerts: { sync: casesConfiguration?.syncAlerts ?? false } }}
|
||||
>
|
||||
<AlertsDataGrid {...dataGridProps} />
|
||||
</CasesContext>
|
||||
) : (
|
||||
<AlertsDataGrid {...dataGridProps} />
|
||||
))}
|
||||
</AlertsTableContextProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Lazy loading helpers
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AlertsTable as default };
|
||||
export type AlertsTable = typeof AlertsTable;
|
|
@ -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 { EuiCheckbox, EuiLoadingSpinner, RenderCellValue } from '@elastic/eui';
|
||||
import React, { ChangeEvent, memo, useCallback } from 'react';
|
||||
import { SELECT_ROW_ARIA_LABEL } from '../translations';
|
||||
import { useAlertsTableContext } from '../contexts/alerts_table_context';
|
||||
import { BulkActionsVerbs } from '../types';
|
||||
|
||||
export const BulkActionsCell = memo(
|
||||
({
|
||||
visibleRowIndex: rowIndex,
|
||||
}: {
|
||||
/**
|
||||
* Defining manually since the {@renderCellValue} type is missing this prop
|
||||
* @external https://github.com/elastic/eui/issues/5811
|
||||
*/
|
||||
visibleRowIndex: number;
|
||||
}) => {
|
||||
const {
|
||||
bulkActionsStore: [{ rowSelection }, updateSelectedRows],
|
||||
} = useAlertsTableContext();
|
||||
const isChecked = rowSelection.has(rowIndex);
|
||||
const isLoading = isChecked && rowSelection.get(rowIndex)?.isLoading;
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
updateSelectedRows({ action: BulkActionsVerbs.add, rowIndex });
|
||||
} else {
|
||||
updateSelectedRows({ action: BulkActionsVerbs.delete, rowIndex });
|
||||
}
|
||||
},
|
||||
[rowIndex, updateSelectedRows]
|
||||
);
|
||||
if (isLoading) {
|
||||
return <EuiLoadingSpinner size="m" data-test-subj="row-loader" />;
|
||||
}
|
||||
|
||||
// NOTE: id is prefixed here to avoid conflicts with labels in other sections in the app.
|
||||
// see https://github.com/elastic/kibana/issues/162837
|
||||
return (
|
||||
<EuiCheckbox
|
||||
id={`bulk-actions-row-cell-${rowIndex}`}
|
||||
aria-label={SELECT_ROW_ARIA_LABEL(rowIndex + 1)}
|
||||
checked={isChecked}
|
||||
onChange={onChange}
|
||||
data-test-subj="bulk-actions-row-cell"
|
||||
/>
|
||||
);
|
||||
}
|
||||
// See props comment above
|
||||
) as unknown as RenderCellValue;
|
|
@ -1,20 +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; 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 { EuiCheckbox } from '@elastic/eui';
|
||||
import React, { ChangeEvent, useContext, useCallback } from 'react';
|
||||
import { BulkActionsVerbs } from '../../../../../types';
|
||||
import React, { ChangeEvent, useCallback } from 'react';
|
||||
import { BulkActionsVerbs } from '../types';
|
||||
import { COLUMN_HEADER_ARIA_LABEL } from '../translations';
|
||||
import { AlertsTableContext } from '../../contexts/alerts_table_context';
|
||||
import { useAlertsTableContext } from '../contexts/alerts_table_context';
|
||||
|
||||
const BulkActionsHeaderComponent: React.FunctionComponent = () => {
|
||||
const {
|
||||
bulkActions: [{ isAllSelected, areAllVisibleRowsSelected }, updateSelectedRows],
|
||||
} = useContext(AlertsTableContext);
|
||||
bulkActionsStore: [{ isAllSelected, areAllVisibleRowsSelected }, updateSelectedRows],
|
||||
} = useAlertsTableContext();
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
|
@ -1,14 +1,15 @@
|
|||
/*
|
||||
* 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 React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { EuiPopover, EuiButtonEmpty, EuiContextMenu } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import React, { useState, useCallback, useMemo, useContext, useEffect } from 'react';
|
||||
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
ALERT_CASE_IDS,
|
||||
ALERT_RULE_NAME,
|
||||
|
@ -16,43 +17,28 @@ import {
|
|||
ALERT_WORKFLOW_ASSIGNEE_IDS,
|
||||
ALERT_WORKFLOW_TAGS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import {
|
||||
Alerts,
|
||||
BulkActionsPanelConfig,
|
||||
BulkActionsVerbs,
|
||||
RowSelection,
|
||||
} from '../../../../../types';
|
||||
import { Alert } from '@kbn/alerting-types';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { BulkActionsPanelConfig, BulkActionsVerbs, RowSelection, TimelineItem } from '../types';
|
||||
import * as i18n from '../translations';
|
||||
import { AlertsTableContext } from '../../contexts/alerts_table_context';
|
||||
import { useAlertsTableContext } from '../contexts/alerts_table_context';
|
||||
|
||||
interface BulkActionsProps {
|
||||
totalItems: number;
|
||||
panels: BulkActionsPanelConfig[];
|
||||
alerts: Alerts;
|
||||
alerts: Alert[];
|
||||
setIsBulkActionsLoading: (loading: boolean) => void;
|
||||
clearSelection: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
// Duplicated just for legacy reasons. Timelines plugin will be removed but
|
||||
// as long as the integration still work with Timelines we have to keep it
|
||||
export interface TimelineItem {
|
||||
_id: string;
|
||||
_index?: string | null;
|
||||
data: TimelineNonEcsData[];
|
||||
ecs: { _id: string; _index: string };
|
||||
}
|
||||
|
||||
export interface TimelineNonEcsData {
|
||||
field: string;
|
||||
value?: string[] | null;
|
||||
settings: SettingsStart;
|
||||
}
|
||||
|
||||
const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';
|
||||
const containerStyles = { display: 'inline-block', position: 'relative' } as const;
|
||||
|
||||
const selectedIdsToTimelineItemMapper = (
|
||||
alerts: Alerts,
|
||||
alerts: Alert[],
|
||||
rowSelection: RowSelection
|
||||
): TimelineItem[] => {
|
||||
return Array.from(rowSelection.keys()).map((rowIndex: number) => {
|
||||
|
@ -61,11 +47,14 @@ const selectedIdsToTimelineItemMapper = (
|
|||
_id: alert._id,
|
||||
_index: alert._index,
|
||||
data: [
|
||||
{ field: ALERT_RULE_NAME, value: alert[ALERT_RULE_NAME] },
|
||||
{ field: ALERT_RULE_UUID, value: alert[ALERT_RULE_UUID] },
|
||||
{ field: ALERT_CASE_IDS, value: alert[ALERT_CASE_IDS] ?? [] },
|
||||
{ field: ALERT_WORKFLOW_TAGS, value: alert[ALERT_WORKFLOW_TAGS] ?? [] },
|
||||
{ field: ALERT_WORKFLOW_ASSIGNEE_IDS, value: alert[ALERT_WORKFLOW_ASSIGNEE_IDS] ?? [] },
|
||||
{ field: ALERT_RULE_NAME, value: alert[ALERT_RULE_NAME] as string[] },
|
||||
{ field: ALERT_RULE_UUID, value: alert[ALERT_RULE_UUID] as string[] },
|
||||
{ field: ALERT_CASE_IDS, value: (alert[ALERT_CASE_IDS] ?? []) as string[] },
|
||||
{ field: ALERT_WORKFLOW_TAGS, value: (alert[ALERT_WORKFLOW_TAGS] ?? []) as string[] },
|
||||
{
|
||||
field: ALERT_WORKFLOW_ASSIGNEE_IDS,
|
||||
value: (alert[ALERT_WORKFLOW_ASSIGNEE_IDS] ?? []) as string[],
|
||||
},
|
||||
],
|
||||
ecs: {
|
||||
_id: alert._id,
|
||||
|
@ -84,12 +73,12 @@ const useBulkActionsToMenuPanelMapper = (
|
|||
clearSelection: BulkActionsProps['clearSelection'],
|
||||
// In case bulk item action changes the alert data and need to refresh table page.
|
||||
refresh: BulkActionsProps['refresh'],
|
||||
alerts: Alerts,
|
||||
alerts: Alert[],
|
||||
closeIfPopoverIsOpen: () => void
|
||||
) => {
|
||||
const {
|
||||
bulkActions: [{ isAllSelected, rowSelection }],
|
||||
} = useContext(AlertsTableContext);
|
||||
bulkActionsStore: [{ isAllSelected, rowSelection }],
|
||||
} = useAlertsTableContext();
|
||||
|
||||
const bulkActionsPanels = useMemo(() => {
|
||||
const bulkActionPanelsToReturn = [];
|
||||
|
@ -153,16 +142,20 @@ const BulkActionsComponent: React.FC<BulkActionsProps> = ({
|
|||
setIsBulkActionsLoading,
|
||||
clearSelection,
|
||||
refresh,
|
||||
settings,
|
||||
}) => {
|
||||
const {
|
||||
bulkActions: [{ rowSelection, isAllSelected }, updateSelectedRows],
|
||||
} = useContext(AlertsTableContext);
|
||||
bulkActionsStore: [{ rowSelection, isAllSelected }, updateSelectedRows],
|
||||
} = useAlertsTableContext();
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
|
||||
const [showClearSelection, setShowClearSelectiong] = useState(false);
|
||||
const defaultNumberFormat = useObservable<string>(
|
||||
useMemo(() => settings.client.get$(DEFAULT_NUMBER_FORMAT), [settings.client]),
|
||||
settings.client.get(DEFAULT_NUMBER_FORMAT)
|
||||
);
|
||||
const [showClearSelection, setShowClearSelection] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowClearSelectiong(isAllSelected);
|
||||
setShowClearSelection(isAllSelected);
|
||||
}, [isAllSelected]);
|
||||
|
||||
const selectedCount = rowSelection.size;
|
||||
|
@ -265,6 +258,6 @@ const BulkActionsComponent: React.FC<BulkActionsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// disabled to be able lazy load
|
||||
// Lazy loading helpers
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default React.memo(BulkActionsComponent);
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Alert } from '@kbn/alerting-types';
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { CasesCell } from './cases_cell';
|
||||
import { AdditionalContext, CellComponentProps, RenderContext } from '../types';
|
||||
import { getCasesMapMock } from '../mocks/cases.mock';
|
||||
import { getMaintenanceWindowsMapMock } from '../mocks/maintenance_windows.mock';
|
||||
import { useCaseViewNavigation } from '../hooks/use_case_view_navigation';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
import { AlertsTableContextProvider } from '../contexts/alerts_table_context';
|
||||
|
||||
jest.mock('../hooks/use_case_view_navigation');
|
||||
|
||||
const useCaseViewNavigationMock = jest.mocked(useCaseViewNavigation);
|
||||
const casesMap = getCasesMapMock();
|
||||
const maintenanceWindowsMap = getMaintenanceWindowsMapMock();
|
||||
const alert: Alert = {
|
||||
_id: 'alert-id',
|
||||
_index: 'alert-index',
|
||||
'kibana.alert.case_ids': ['test-id'],
|
||||
};
|
||||
|
||||
const props = createPartialObjectMock<CellComponentProps>({
|
||||
isLoading: false,
|
||||
alert,
|
||||
cases: casesMap,
|
||||
maintenanceWindows: maintenanceWindowsMap,
|
||||
columnId: 'kibana.alert.case_ids',
|
||||
showAlertStatusWithFlapping: false,
|
||||
});
|
||||
|
||||
const context = createPartialObjectMock<RenderContext<AdditionalContext>>({
|
||||
services: {
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
},
|
||||
});
|
||||
|
||||
const navigateToCaseView = jest.fn();
|
||||
useCaseViewNavigationMock.mockReturnValue({ navigateToCaseView });
|
||||
|
||||
const TestComponent = (_props: CellComponentProps) => (
|
||||
<IntlProvider locale="en">
|
||||
<AlertsTableContextProvider value={context}>
|
||||
<CasesCell {..._props} />
|
||||
</AlertsTableContextProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('CasesCell', () => {
|
||||
it('renders the cases cell', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
expect(screen.getByText('Test case')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the loading skeleton', async () => {
|
||||
render(<TestComponent {...props} isLoading={true} />);
|
||||
expect(screen.getByTestId('cases-cell-loading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple cases correctly', async () => {
|
||||
render(
|
||||
<TestComponent
|
||||
{...props}
|
||||
alert={{ ...alert, 'kibana.alert.case_ids': ['test-id', 'test-id-2'] }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test case')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test case 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render a case that it is in the map but not in the alerts data', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
|
||||
expect(screen.getByText('Test case')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Test case 2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show any cases when the alert does not have any case ids', async () => {
|
||||
render(<TestComponent {...props} alert={{ ...alert, 'kibana.alert.case_ids': [] }} />);
|
||||
|
||||
expect(screen.queryByText('Test case')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Test case 2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does show the default value when the alert does not have any case ids', async () => {
|
||||
render(<TestComponent {...props} alert={{ ...alert, 'kibana.alert.case_ids': [] }} />);
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show any cases when the alert has invalid case ids', async () => {
|
||||
render(
|
||||
<TestComponent {...props} alert={{ ...alert, 'kibana.alert.case_ids': ['not-exist'] }} />
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('cases-cell-link')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does show the default value when the alert has invalid case ids', async () => {
|
||||
render(
|
||||
<TestComponent {...props} alert={{ ...alert, 'kibana.alert.case_ids': ['not-exist'] }} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('--')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the cases tooltip', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
expect(screen.getByText('Test case')).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(screen.getByText('Test case'));
|
||||
|
||||
expect(await screen.findByTestId('cases-components-tooltip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to the case correctly', async () => {
|
||||
render(<TestComponent {...props} />);
|
||||
expect(screen.getByText('Test case')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('Test case'));
|
||||
expect(navigateToCaseView).toBeCalledWith({ caseId: 'test-id' });
|
||||
});
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
|
@ -10,9 +12,10 @@ import { EuiLink, EuiSkeletonText } from '@elastic/eui';
|
|||
import { Tooltip as CaseTooltip } from '@kbn/cases-components';
|
||||
import type { CaseTooltipContentProps } from '@kbn/cases-components';
|
||||
import { ALERT_CASE_IDS } from '@kbn/rule-data-utils';
|
||||
import { CellComponentProps } from '../types';
|
||||
import { useCaseViewNavigation } from './use_case_view_navigation';
|
||||
import { Case } from '../hooks/apis/bulk_get_cases';
|
||||
import { useAlertsTableContext } from '../contexts/alerts_table_context';
|
||||
import type { CellComponent } from '../types';
|
||||
import { useCaseViewNavigation } from '../hooks/use_case_view_navigation';
|
||||
import type { Case } from '../apis/bulk_get_cases';
|
||||
|
||||
const formatCase = (theCase: Case): CaseTooltipContentProps => ({
|
||||
title: theCase.title,
|
||||
|
@ -26,14 +29,17 @@ const formatCase = (theCase: Case): CaseTooltipContentProps => ({
|
|||
totalComments: theCase.totalComment,
|
||||
});
|
||||
|
||||
const CasesCellComponent: React.FC<CellComponentProps> = (props) => {
|
||||
export const CasesCell: CellComponent = memo((props) => {
|
||||
const { isLoading, alert, cases, caseAppId } = props;
|
||||
const { navigateToCaseView } = useCaseViewNavigation(caseAppId);
|
||||
const {
|
||||
services: { application },
|
||||
} = useAlertsTableContext();
|
||||
const { navigateToCaseView } = useCaseViewNavigation(application, caseAppId);
|
||||
|
||||
const caseIds = (alert && alert[ALERT_CASE_IDS]) ?? [];
|
||||
const caseIds = (alert && (alert[ALERT_CASE_IDS] as string[])) ?? [];
|
||||
|
||||
const validCases = caseIds
|
||||
.map((id) => cases.get(id))
|
||||
.map((id) => cases?.get(id))
|
||||
.filter((theCase): theCase is Case => theCase != null);
|
||||
|
||||
return (
|
||||
|
@ -53,8 +59,4 @@ const CasesCellComponent: React.FC<CellComponentProps> = (props) => {
|
|||
: '--'}
|
||||
</EuiSkeletonText>
|
||||
);
|
||||
};
|
||||
|
||||
CasesCellComponent.displayName = 'CasesCell';
|
||||
|
||||
export const CasesCell = memo(CasesCellComponent);
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 React, { useEffect } from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { EuiDataGridCellPopoverElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { AlertsTableContextProvider } from '../contexts/alerts_table_context';
|
||||
import { CellPopoverHost } from './cell_popover_host';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
import { mockRenderContext } from '../mocks/context.mock';
|
||||
|
||||
const props = createPartialObjectMock<EuiDataGridCellPopoverElementProps>({
|
||||
rowIndex: 0,
|
||||
DefaultCellPopover: jest.fn().mockReturnValue(<div data-test-subj="defaultCellPopover" />),
|
||||
});
|
||||
|
||||
describe('CellPopoverHost', () => {
|
||||
it('should render the renderCellPopover when provided', () => {
|
||||
render(
|
||||
<AlertsTableContextProvider
|
||||
value={{
|
||||
...mockRenderContext,
|
||||
renderCellPopover: jest.fn(() => <div data-test-subj="renderCellPopover" />),
|
||||
}}
|
||||
>
|
||||
<CellPopoverHost {...props} />
|
||||
</AlertsTableContextProvider>
|
||||
);
|
||||
expect(screen.getByTestId('renderCellPopover')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should catch errors from the custom CellPopover', async () => {
|
||||
const CustomCellPopover = () => {
|
||||
useEffect(() => {
|
||||
throw new Error('test error');
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AlertsTableContextProvider
|
||||
value={{
|
||||
...mockRenderContext,
|
||||
renderCellPopover: CustomCellPopover,
|
||||
}}
|
||||
>
|
||||
<CellPopoverHost {...props} />
|
||||
</AlertsTableContextProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(await screen.findByTestId('errorCell')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the DefaultCellPopover when renderCellPopover is not provided', () => {
|
||||
render(
|
||||
<AlertsTableContextProvider value={mockRenderContext}>
|
||||
<CellPopoverHost {...props} />
|
||||
</AlertsTableContextProvider>
|
||||
);
|
||||
expect(screen.getByTestId('defaultCellPopover')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { EuiDataGridCellPopoverElementProps } from '@elastic/eui/src/components/datagrid/data_grid_types';
|
||||
import React from 'react';
|
||||
import { useAlertsTableContext } from '../contexts/alerts_table_context';
|
||||
import { ErrorBoundary } from './error_boundary';
|
||||
import { ErrorCell } from './error_cell';
|
||||
|
||||
/**
|
||||
* Entry point for rendering cell popovers
|
||||
*
|
||||
* Wraps the provided `CellPopover` with an `ErrorBoundary` to catch any errors
|
||||
*/
|
||||
export const CellPopoverHost = (props: EuiDataGridCellPopoverElementProps) => {
|
||||
const { rowIndex, DefaultCellPopover } = props;
|
||||
const renderContext = useAlertsTableContext();
|
||||
const { pageSize, pageIndex, alerts, renderCellPopover: CellPopover } = renderContext;
|
||||
|
||||
const idx = rowIndex - pageSize * pageIndex;
|
||||
const alert = alerts[idx];
|
||||
if (alert && CellPopover) {
|
||||
return (
|
||||
<ErrorBoundary fallback={ErrorCell}>
|
||||
<CellPopover {...renderContext} {...props} alert={alert} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
return <DefaultCellPopover {...props} />;
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 React, { ComponentProps, FC } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { CellValueHost } from './cell_value_host';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
import { ALERT_CASE_IDS, ALERT_MAINTENANCE_WINDOW_IDS, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import { DefaultCellValue } from './default_cell_value';
|
||||
|
||||
jest.mock('./system_cell', () => {
|
||||
const original = jest.requireActual('./system_cell');
|
||||
return {
|
||||
...original,
|
||||
SystemCell: jest.fn(() => <div data-test-subj="systemCell" />),
|
||||
};
|
||||
});
|
||||
jest.mock('./default_cell_value');
|
||||
jest.mocked(DefaultCellValue as FC).mockReturnValue(<div data-test-subj="defaultCell" />);
|
||||
|
||||
const props = createPartialObjectMock<ComponentProps<typeof CellValueHost>>({
|
||||
isLoading: false,
|
||||
alerts: [
|
||||
{
|
||||
_id: 'test',
|
||||
_index: 'alerts',
|
||||
[ALERT_STATUS]: ['active'],
|
||||
},
|
||||
],
|
||||
oldAlertsData: [],
|
||||
ecsAlertsData: [],
|
||||
showAlertStatusWithFlapping: false,
|
||||
casesConfig: undefined,
|
||||
rowIndex: 0,
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
describe('CellValueHost', () => {
|
||||
it.each([ALERT_STATUS, ALERT_CASE_IDS, ALERT_MAINTENANCE_WINDOW_IDS])(
|
||||
'should render a SystemCell for cases, maintenance windows, and alert status',
|
||||
(columnId) => {
|
||||
render(<CellValueHost {...props} columnId={columnId} />);
|
||||
expect(screen.getByTestId('systemCell')).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
it('should render the provided renderCellValue for other fields', () => {
|
||||
render(
|
||||
<CellValueHost
|
||||
{...props}
|
||||
columnId="otherField"
|
||||
renderCellValue={jest.fn(() => (
|
||||
<div data-test-subj="customRenderCellValue" />
|
||||
))}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('customRenderCellValue')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a DefaultCell for other fields when a custom renderCellValue is not defined', () => {
|
||||
render(<CellValueHost {...props} columnId="otherField" />);
|
||||
expect(screen.getByTestId('defaultCell')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a loading skeleton when the isLoading prop is true and the alert is not available yet', () => {
|
||||
render(<CellValueHost {...props} columnId="otherField" isLoading alerts={[]} />);
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiSkeletonText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { GetAlertsTableProp, SystemCellId } from '../types';
|
||||
import { DefaultCellValue } from './default_cell_value';
|
||||
import { SystemCell, systemCells } from './system_cell';
|
||||
import { ErrorBoundary } from './error_boundary';
|
||||
import { ErrorCell } from './error_cell';
|
||||
|
||||
/**
|
||||
* Entry point for rendering cell values
|
||||
*
|
||||
* Renders a SystemCell for cases, maintenance windows, and alert status, the `renderCellValue`
|
||||
* provided in the `AlertsTableProps` for other fields or the default cell value renderer otherwise.
|
||||
* When the alerts or the related cases or maintenance windows are loading, a skeleton text is rendered.
|
||||
*/
|
||||
export const CellValueHost: GetAlertsTableProp<'renderCellValue'> = (props) => {
|
||||
const {
|
||||
columnId,
|
||||
renderCellValue: CellValue = DefaultCellValue,
|
||||
isLoading,
|
||||
alerts,
|
||||
oldAlertsData,
|
||||
ecsAlertsData,
|
||||
cases,
|
||||
maintenanceWindows,
|
||||
showAlertStatusWithFlapping,
|
||||
casesConfig,
|
||||
rowIndex,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
} = props;
|
||||
const idx = rowIndex - pageSize * pageIndex;
|
||||
const alert = alerts[idx];
|
||||
const legacyAlert = oldAlertsData[idx];
|
||||
const ecsAlert = ecsAlertsData[idx];
|
||||
if (isSystemCell(columnId) && alert) {
|
||||
return (
|
||||
<SystemCell
|
||||
{...props}
|
||||
alert={alert}
|
||||
columnId={columnId}
|
||||
isLoading={isLoading}
|
||||
cases={cases}
|
||||
maintenanceWindows={maintenanceWindows}
|
||||
showAlertStatusWithFlapping={showAlertStatusWithFlapping ?? false}
|
||||
caseAppId={casesConfig?.appId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (alert) {
|
||||
return (
|
||||
<ErrorBoundary fallback={ErrorCell}>
|
||||
<CellValue {...props} alert={alert} legacyAlert={legacyAlert} ecsAlert={ecsAlert} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
return <EuiSkeletonText lines={1} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const isSystemCell = (columnId: string): columnId is SystemCellId => {
|
||||
return systemCells.includes(columnId as SystemCellId);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL } from '../translations';
|
||||
|
||||
export const ControlColumnHeaderCell = memo(() => {
|
||||
return (
|
||||
<span data-test-subj="expandColumnHeaderLabel">
|
||||
{ALERTS_TABLE_CONTROL_COLUMNS_ACTIONS_LABEL}
|
||||
</span>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { DefaultAlertActions } from './default_alert_actions';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AdditionalContext, AlertActionsProps, RenderContext } from '../types';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
import { AlertsTableContextProvider } from '../contexts/alerts_table_context';
|
||||
|
||||
jest.mock('@kbn/alerts-ui-shared/src/common/hooks/use_load_rule_types_query', () => ({
|
||||
useLoadRuleTypesQuery: jest.fn(),
|
||||
}));
|
||||
jest.mock('./view_rule_details_alert_action', () => {
|
||||
return {
|
||||
ViewRuleDetailsAlertAction: () => (
|
||||
<div data-test-subj="viewRuleDetailsAlertAction">{'ViewRuleDetailsAlertAction'}</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
jest.mock('./view_alert_details_alert_action', () => {
|
||||
return {
|
||||
ViewAlertDetailsAlertAction: () => (
|
||||
<div data-test-subj="viewAlertDetailsAlertAction">{'ViewAlertDetailsAlertAction'}</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
jest.mock('./mute_alert_action', () => {
|
||||
return { MuteAlertAction: () => <div data-test-subj="muteAlertAction">{'MuteAlertAction'}</div> };
|
||||
});
|
||||
jest.mock('./mark_as_untracked_alert_action', () => {
|
||||
return {
|
||||
MarkAsUntrackedAlertAction: () => (
|
||||
<div data-test-subj="markAsUntrackedAlertAction">{'MarkAsUntrackedAlertAction'}</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const { useLoadRuleTypesQuery } = jest.requireMock(
|
||||
'@kbn/alerts-ui-shared/src/common/hooks/use_load_rule_types_query'
|
||||
);
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
const notifications = notificationServiceMock.createStartContract();
|
||||
const props = createPartialObjectMock<AlertActionsProps>({
|
||||
alert: {},
|
||||
refresh: jest.fn(),
|
||||
});
|
||||
|
||||
const context = createPartialObjectMock<RenderContext<AdditionalContext>>({
|
||||
services: {
|
||||
http,
|
||||
notifications,
|
||||
},
|
||||
});
|
||||
|
||||
const TestComponent = (_props: AlertActionsProps) => (
|
||||
<AlertsTableContextProvider value={context}>
|
||||
<DefaultAlertActions {..._props} />
|
||||
</AlertsTableContextProvider>
|
||||
);
|
||||
|
||||
describe('DefaultAlertActions', () => {
|
||||
it('should show "Mute" and "Marked as untracked" option', async () => {
|
||||
useLoadRuleTypesQuery.mockReturnValue({ authorizedToCreateAnyRules: true });
|
||||
|
||||
render(<TestComponent {...props} />);
|
||||
|
||||
expect(await screen.findByText('MuteAlertAction')).toBeInTheDocument();
|
||||
expect(await screen.findByText('MarkAsUntrackedAlertAction')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide "Mute" and "Marked as untracked" option', async () => {
|
||||
useLoadRuleTypesQuery.mockReturnValue({ authorizedToCreateAnyRules: false });
|
||||
|
||||
render(<TestComponent {...props} />);
|
||||
|
||||
expect(screen.queryByText('MuteAlertAction')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('MarkAsUntrackedAlertAction')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { useLoadRuleTypesQuery } from '@kbn/alerts-ui-shared/src/common/hooks';
|
||||
import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/alerts_query_context';
|
||||
import { ViewRuleDetailsAlertAction } from './view_rule_details_alert_action';
|
||||
import type { AdditionalContext, AlertActionsProps } from '../types';
|
||||
import { ViewAlertDetailsAlertAction } from './view_alert_details_alert_action';
|
||||
import { MuteAlertAction } from './mute_alert_action';
|
||||
import { MarkAsUntrackedAlertAction } from './mark_as_untracked_alert_action';
|
||||
import { useAlertsTableContext } from '../contexts/alerts_table_context';
|
||||
|
||||
/**
|
||||
* Common alerts table row actions
|
||||
*/
|
||||
export const DefaultAlertActions = <AC extends AdditionalContext = AdditionalContext>(
|
||||
props: AlertActionsProps<AC>
|
||||
) => {
|
||||
const {
|
||||
services: {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
},
|
||||
} = useAlertsTableContext();
|
||||
const { authorizedToCreateAnyRules } = useLoadRuleTypesQuery({
|
||||
filteredRuleTypes: [],
|
||||
http,
|
||||
toasts,
|
||||
context: AlertsQueryContext,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewRuleDetailsAlertAction {...props} />
|
||||
<ViewAlertDetailsAlertAction {...props} />
|
||||
{authorizedToCreateAnyRules && <MarkAsUntrackedAlertAction {...props} />}
|
||||
{authorizedToCreateAnyRules && <MuteAlertAction {...props} />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,17 +1,27 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mount } from 'enzyme';
|
||||
import type { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { getDefaultAlertFlyout } from './default_alerts_flyout';
|
||||
import { AlertsTableFlyoutBaseProps } from '../../../..';
|
||||
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import {
|
||||
AdditionalContext,
|
||||
AlertsTableFlyoutBaseProps,
|
||||
FlyoutSectionProps,
|
||||
RenderContext,
|
||||
} from '../types';
|
||||
import { DefaultAlertsFlyoutBody } from './default_alerts_flyout';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { AlertsTableContextProvider } from '../contexts/alerts_table_context';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -84,11 +94,27 @@ const tabsData = [
|
|||
{ name: 'Table', subj: 'tableTab' },
|
||||
];
|
||||
|
||||
const context = createPartialObjectMock<RenderContext<AdditionalContext>>({
|
||||
services: {
|
||||
http: httpServiceMock.createStartContract(),
|
||||
fieldFormats: fieldFormatsMock,
|
||||
},
|
||||
});
|
||||
|
||||
describe('DefaultAlertsFlyout', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
beforeAll(async () => {
|
||||
const { body: FlyoutBody } = getDefaultAlertFlyout(columns, (_columnId, value) => value)();
|
||||
wrapper = mount(<FlyoutBody alert={alert} isLoading={false} />) as ReactWrapper;
|
||||
wrapper = mount(
|
||||
<AlertsTableContextProvider value={context}>
|
||||
<DefaultAlertsFlyoutBody
|
||||
{...createPartialObjectMock<FlyoutSectionProps>({
|
||||
alert,
|
||||
isLoading: false,
|
||||
columns,
|
||||
})}
|
||||
/>
|
||||
</AlertsTableContextProvider>
|
||||
) as ReactWrapper;
|
||||
await waitFor(() => wrapper.update());
|
||||
});
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiDescriptionList, EuiPanel, EuiTabbedContentTab, EuiTitle } from '@elastic/eui';
|
||||
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ScrollableFlyoutTabbedContent, AlertFieldsTable } from '@kbn/alerts-ui-shared';
|
||||
import { AdditionalContext, FlyoutSectionProps } from '../types';
|
||||
import { defaultAlertsTableColumns } from '../configuration';
|
||||
import { DefaultCellValue } from './default_cell_value';
|
||||
|
||||
export const DefaultAlertsFlyoutHeader = <AC extends AdditionalContext>({
|
||||
alert,
|
||||
}: FlyoutSectionProps<AC>) => {
|
||||
return (
|
||||
<EuiTitle size="s">
|
||||
<h3>{(alert[ALERT_RULE_NAME]?.[0] as string) ?? 'Unknown'}</h3>
|
||||
</EuiTitle>
|
||||
);
|
||||
};
|
||||
|
||||
type TabId = 'overview' | 'table';
|
||||
|
||||
export const DefaultAlertsFlyoutBody = <AC extends AdditionalContext>(
|
||||
props: FlyoutSectionProps<AC>
|
||||
) => {
|
||||
const { alert, columns } = props;
|
||||
const overviewTab = useMemo(
|
||||
() => ({
|
||||
id: 'overview',
|
||||
'data-test-subj': 'overviewTab',
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.overview', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
content: (
|
||||
<EuiPanel hasShadow={false} data-test-subj="overviewTabPanel">
|
||||
<EuiDescriptionList
|
||||
listItems={(columns ?? defaultAlertsTableColumns).map((column) => {
|
||||
const value = alert[column.id]?.[0];
|
||||
|
||||
return {
|
||||
title: (column.displayAsText as string) ?? column.id,
|
||||
description:
|
||||
value != null ? (
|
||||
<DefaultCellValue columnId={column.id} alert={props.alert} />
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
};
|
||||
})}
|
||||
type="column"
|
||||
columnWidths={[1, 3]}
|
||||
/>
|
||||
</EuiPanel>
|
||||
),
|
||||
}),
|
||||
[alert, columns, props]
|
||||
);
|
||||
|
||||
const tableTab = useMemo(
|
||||
() => ({
|
||||
id: 'table',
|
||||
'data-test-subj': 'tableTab',
|
||||
name: i18n.translate('xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.table', {
|
||||
defaultMessage: 'Table',
|
||||
}),
|
||||
content: (
|
||||
<EuiPanel hasShadow={false} data-test-subj="tableTabPanel">
|
||||
<AlertFieldsTable alert={alert} />
|
||||
</EuiPanel>
|
||||
),
|
||||
}),
|
||||
[alert]
|
||||
);
|
||||
|
||||
const tabs = useMemo(() => [overviewTab, tableTab], [overviewTab, tableTab]);
|
||||
const [selectedTabId, setSelectedTabId] = useState<TabId>('overview');
|
||||
const handleTabClick = useCallback(
|
||||
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as TabId),
|
||||
[]
|
||||
);
|
||||
|
||||
const selectedTab = useMemo(
|
||||
() => tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0],
|
||||
[tabs, selectedTabId]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollableFlyoutTabbedContent
|
||||
tabs={tabs}
|
||||
selectedTab={selectedTab}
|
||||
onTabClick={handleTabClick}
|
||||
expand
|
||||
data-test-subj="defaultAlertFlyoutTabs"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import { DefaultCell } from './default_cell';
|
||||
import { CellComponentProps } from '../types';
|
||||
import { getCasesMapMock } from '../mocks/cases.mock';
|
||||
import { getMaintenanceWindowsMapMock } from '../mocks/maintenance_windows.mock';
|
||||
import { createPartialObjectMock } from '../utils/test';
|
||||
|
||||
const casesMap = getCasesMapMock();
|
||||
const maintenanceWindowsMap = getMaintenanceWindowsMapMock();
|
||||
const alert: Alert = {
|
||||
_id: 'alert-id',
|
||||
_index: 'alert-index',
|
||||
'kibana.alert.status': ['active'],
|
||||
};
|
||||
|
||||
const props = createPartialObjectMock<CellComponentProps>({
|
||||
isLoading: false,
|
||||
alert,
|
||||
cases: casesMap,
|
||||
maintenanceWindows: maintenanceWindowsMap,
|
||||
columnId: 'kibana.alert.status',
|
||||
showAlertStatusWithFlapping: false,
|
||||
});
|
||||
|
||||
describe('DefaultCell', () => {
|
||||
it('shows the value', async () => {
|
||||
render(<DefaultCell {...props} />);
|
||||
expect(screen.getByText('active')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty tag if the value is empty', async () => {
|
||||
render(<DefaultCell {...props} alert={{ ...alert, 'kibana.alert.status': [] }} />);
|
||||
expect(screen.getByText('--')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows multiple values', async () => {
|
||||
render(
|
||||
<DefaultCell
|
||||
{...props}
|
||||
alert={{ ...alert, 'kibana.alert.status': ['active', 'recovered'] }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('active, recovered')).toBeInTheDocument();
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue