Skip to content

Commit d46bd47

Browse files
[AI4DSOC] Alert summary dataview (#215265)
## Summary This PR continues and finalizes the pre-work done in #214889. Once this PR is merged, the actual alert summary page content implementation will begin. We need a dataView to be created before being able to fetch any data. The `wrapper.tsx` component creates a dataView. - While the dataView is being created, a loading skeleton mimicking the future layout of the alert summary page is rendered. - If the dataView fails to be correctly created (meaning if it comes back undefined or without an id) we show an error message. - If the dataView is correctly created, we continue to the alert summary page (currently just a div) https://github.com/user-attachments/assets/f1c8f63e-30a0-4186-94b6-f18a18a89218 ![Screenshot 2025-03-20 at 12 09 02 AM](https://github.com/user-attachments/assets/8ad6055b-1788-4372-afc1-af33e75cb29a) ## How to test This needs to be ran in Serverless: - `yarn es serverless --projectType security` - `yarn serverless-security --no-base-path` You also need to enable the AI for SOC tier, by adding the following to your `serverless.security.dev.yaml` file: ``` xpack.securitySolutionServerless.productTypes: [ { product_line: 'ai_soc', product_tier: 'search_ai_lake' }, ] ``` Use one of these Serverless users: - `platform_engineer` - `endpoint_operations_analyst` - `endpoint_policy_manager` - `admin` - `system_indices_superuser` ### Notes You'll need to either have some AI for SOC integrations installed, or more easily you can change the `alert_summary.tsx` line 38 from `if (installedPackages.length === 0) {` to `if (installedPackages.length > 0) {` to force the wrapper component to render. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [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
1 parent d7d690c commit d46bd47

File tree

3 files changed

+202
-12
lines changed

3 files changed

+202
-12
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { act, render } from '@testing-library/react';
10+
import type { PackageListItem } from '@kbn/fleet-plugin/common';
11+
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
12+
import {
13+
CONTENT_TEST_ID,
14+
DATA_VIEW_ERROR_TEST_ID,
15+
DATA_VIEW_LOADING_PROMPT_TEST_ID,
16+
SKELETON_TEST_ID,
17+
Wrapper,
18+
} from './wrapper';
19+
import { useKibana } from '../../../common/lib/kibana';
20+
21+
jest.mock('../../../common/lib/kibana');
22+
23+
const packages: PackageListItem[] = [
24+
{
25+
id: 'splunk',
26+
name: 'splunk',
27+
status: installationStatuses.NotInstalled,
28+
title: 'Splunk',
29+
version: '',
30+
},
31+
];
32+
33+
describe('<Wrapper />', () => {
34+
it('should render a loading skeleton while creating the dataView', async () => {
35+
(useKibana as jest.Mock).mockReturnValue({
36+
services: {
37+
data: {
38+
dataViews: {
39+
create: jest.fn(),
40+
clearInstanceCache: jest.fn(),
41+
},
42+
},
43+
},
44+
});
45+
46+
await act(async () => {
47+
const { getByTestId } = render(<Wrapper packages={packages} />);
48+
49+
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
50+
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
51+
});
52+
});
53+
54+
it('should render an error if the dataView fail to be created correctly', async () => {
55+
(useKibana as jest.Mock).mockReturnValue({
56+
services: {
57+
data: {
58+
dataViews: {
59+
create: jest.fn().mockReturnValue(undefined),
60+
clearInstanceCache: jest.fn(),
61+
},
62+
},
63+
},
64+
});
65+
66+
jest.mock('react', () => ({
67+
...jest.requireActual('react'),
68+
useEffect: jest.fn((f) => f()),
69+
}));
70+
71+
await act(async () => {
72+
const { getByTestId } = render(<Wrapper packages={packages} />);
73+
74+
await new Promise(process.nextTick);
75+
76+
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
77+
expect(getByTestId(DATA_VIEW_ERROR_TEST_ID)).toHaveTextContent('Unable to create data view');
78+
});
79+
});
80+
81+
it('should render the content if the dataView is created correctly', async () => {
82+
(useKibana as jest.Mock).mockReturnValue({
83+
services: {
84+
data: {
85+
dataViews: {
86+
create: jest.fn().mockReturnValue({ id: 'id' }),
87+
clearInstanceCache: jest.fn(),
88+
},
89+
},
90+
},
91+
});
92+
93+
jest.mock('react', () => ({
94+
...jest.requireActual('react'),
95+
useEffect: jest.fn((f) => f()),
96+
}));
97+
98+
await act(async () => {
99+
const { getByTestId } = render(<Wrapper packages={packages} />);
100+
101+
await new Promise(process.nextTick);
102+
103+
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
104+
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
105+
});
106+
});
107+
});

x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,29 @@
55
* 2.0.
66
*/
77

8-
import React, { memo } from 'react';
8+
import React, { memo, useEffect, useState } from 'react';
9+
import {
10+
EuiEmptyPrompt,
11+
EuiHorizontalRule,
12+
EuiSkeletonLoading,
13+
EuiSkeletonRectangle,
14+
EuiSpacer,
15+
} from '@elastic/eui';
16+
import { i18n } from '@kbn/i18n';
17+
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
918
import type { PackageListItem } from '@kbn/fleet-plugin/common';
10-
import { EuiText } from '@elastic/eui';
19+
import { useKibana } from '../../../common/lib/kibana';
20+
21+
const DATAVIEW_ERROR = i18n.translate('xpack.securitySolution.alertSummary.dataViewError', {
22+
defaultMessage: 'Unable to create data view',
23+
});
1124

1225
export const DATA_VIEW_LOADING_PROMPT_TEST_ID = 'alert-summary-data-view-loading-prompt';
26+
export const DATA_VIEW_ERROR_TEST_ID = 'alert-summary-data-view-error';
27+
export const SKELETON_TEST_ID = 'alert-summary-skeleton';
28+
export const CONTENT_TEST_ID = 'alert-summary-content';
29+
30+
const dataViewSpec: DataViewSpec = { title: '.alerts-security.alerts-default' };
1331

1432
export interface WrapperProps {
1533
/**
@@ -19,12 +37,64 @@ export interface WrapperProps {
1937
}
2038

2139
/**
22-
* Creates a new dataView with the alert indices while displaying a loading skeleton.
23-
* Display the alert summary page content if the dataView is correctly created.
24-
* This page is rendered when there are AI for SOC packages installed.
40+
* Creates a new adhoc dataView for the alert summary page. The dataView is created just with the alert indices.
41+
* During the creating, we display a loading skeleton, mimicking the future alert summary page content.
42+
* Once the dataView is correctly created, we render the content.
43+
* If the creation fails, we show an error message.
2544
*/
2645
export const Wrapper = memo(({ packages }: WrapperProps) => {
27-
return <EuiText data-test-subj={DATA_VIEW_LOADING_PROMPT_TEST_ID}>{'Wrapper'}</EuiText>;
46+
const { data } = useKibana().services;
47+
const [dataView, setDataView] = useState<DataView | undefined>(undefined);
48+
const [loading, setLoading] = useState<boolean>(true);
49+
50+
useEffect(() => {
51+
let dv: DataView;
52+
const createDataView = async () => {
53+
dv = await data.dataViews.create(dataViewSpec);
54+
setDataView(dv);
55+
setLoading(false);
56+
};
57+
createDataView();
58+
59+
// clearing after leaving the page
60+
return () => {
61+
if (dv?.id) {
62+
data.dataViews.clearInstanceCache(dv?.id);
63+
}
64+
};
65+
}, [data.dataViews]);
66+
67+
return (
68+
<EuiSkeletonLoading
69+
data-test-subj={DATA_VIEW_LOADING_PROMPT_TEST_ID}
70+
isLoading={loading}
71+
loadingContent={
72+
<div data-test-subj={SKELETON_TEST_ID}>
73+
<EuiSkeletonRectangle height={50} width="100%" />
74+
<EuiHorizontalRule />
75+
<EuiSkeletonRectangle height={50} width="100%" />
76+
<EuiSpacer />
77+
<EuiSkeletonRectangle height={275} width="100%" />
78+
<EuiSpacer />
79+
<EuiSkeletonRectangle height={600} width="100%" />
80+
</div>
81+
}
82+
loadedContent={
83+
<>
84+
{!dataView || !dataView.id ? (
85+
<EuiEmptyPrompt
86+
color="danger"
87+
data-test-subj={DATA_VIEW_ERROR_TEST_ID}
88+
iconType="error"
89+
title={<h2>{DATAVIEW_ERROR}</h2>}
90+
/>
91+
) : (
92+
<div data-test-subj={CONTENT_TEST_ID}>{'wrapper'}</div>
93+
)}
94+
</>
95+
}
96+
/>
97+
);
2898
});
2999

30100
Wrapper.displayName = 'Wrapper';

x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
*/
77

88
import React from 'react';
9-
import { render } from '@testing-library/react';
9+
import { act, render } from '@testing-library/react';
1010
import { AlertSummaryPage, LOADING_INTEGRATIONS_TEST_ID } from './alert_summary';
1111
import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations';
1212
import { LANDING_PAGE_PROMPT_TEST_ID } from '../../components/alert_summary/landing_page/landing_page';
1313
import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url';
1414
import { DATA_VIEW_LOADING_PROMPT_TEST_ID } from '../../components/alert_summary/wrapper';
15+
import { useKibana } from '../../../common/lib/kibana';
1516

1617
jest.mock('../../hooks/alert_summary/use_fetch_integrations');
1718
jest.mock('../../../common/hooks/use_add_integrations_url');
19+
jest.mock('../../../common/lib/kibana');
1820

1921
describe('<AlertSummaryPage />', () => {
2022
it('should render loading logo', () => {
@@ -41,16 +43,27 @@ describe('<AlertSummaryPage />', () => {
4143
expect(getByTestId(LANDING_PAGE_PROMPT_TEST_ID)).toBeInTheDocument();
4244
});
4345

44-
it('should render wrapper if packages are installed', () => {
46+
it('should render wrapper if packages are installed', async () => {
47+
(useKibana as jest.Mock).mockReturnValue({
48+
services: {
49+
data: {
50+
dataViews: {
51+
create: jest.fn(),
52+
},
53+
},
54+
},
55+
});
4556
(useFetchIntegrations as jest.Mock).mockReturnValue({
4657
availablePackages: [],
4758
installedPackages: [{ id: 'id' }],
4859
isLoading: false,
4960
});
5061

51-
const { getByTestId, queryByTestId } = render(<AlertSummaryPage />);
52-
expect(queryByTestId(LOADING_INTEGRATIONS_TEST_ID)).not.toBeInTheDocument();
53-
expect(queryByTestId(LANDING_PAGE_PROMPT_TEST_ID)).not.toBeInTheDocument();
54-
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
62+
await act(async () => {
63+
const { getByTestId, queryByTestId } = render(<AlertSummaryPage />);
64+
expect(queryByTestId(LOADING_INTEGRATIONS_TEST_ID)).not.toBeInTheDocument();
65+
expect(queryByTestId(LANDING_PAGE_PROMPT_TEST_ID)).not.toBeInTheDocument();
66+
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
67+
});
5568
});
5669
});

0 commit comments

Comments
 (0)