mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[AI4DSOR] Alert summary integrations section (#215266)
## Summary This PR adds the integration section at the top of the alert summary page. This section shows the installed AI for SOC integrations and an `Add integration` button. Clicking on the button navigates to the fleet's page. In each integration card, we show the integration name, its logo as well as the last activity time. This last activity value is retrieve as follow: - fetch all dataStreams (see [this api documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-fleet-data-streams)) - find all the dataStreams that are related to the installed integrations (via the `package` property) - from all the matching dataStreams, take the most recently updated (via the `last_activity_ms` value  https://github.com/user-attachments/assets/7c67e629-e4d3-4ba2-b756-b9ba81e7a667 ## 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 - update `42` of the same `alert_summary.tsx` file from `return <Wrapper packages={installedPackages} />;` to `return <Wrapper packages={availablePackages} />;` to be able to see some packages Also you'll dataStreams if you want to be able to test the last activity value. Easiest would probably be to mock the call return value following [the documentation](https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-fleet-data-streams). ### 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 https://github.com/elastic/security-team/issues/11955
This commit is contained in:
parent
3a63089dc7
commit
59c8d19d95
10 changed files with 512 additions and 0 deletions
|
@ -122,6 +122,7 @@ export type {
|
|||
// Models
|
||||
Agent,
|
||||
AgentStatus,
|
||||
DataStream,
|
||||
FleetServerAgentMetadata,
|
||||
AgentMetadata,
|
||||
NewAgentPolicy,
|
||||
|
|
|
@ -91,6 +91,7 @@ export const AvailablePackagesHook = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export { useGetDataStreams } from './hooks/use_request/data_stream';
|
||||
export { useGetPackagesQuery } from './hooks/use_request/epm';
|
||||
export { useGetSettingsQuery } from './hooks/use_request/settings';
|
||||
export { useLink } from './hooks/use_link';
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import {
|
||||
IntegrationCard,
|
||||
LAST_ACTIVITY_LOADING_SKELETON_TEST_ID,
|
||||
LAST_ACTIVITY_VALUE_TEST_ID,
|
||||
} from './integration_card';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public');
|
||||
|
||||
const dataTestSubj = 'test-id';
|
||||
const integration: PackageListItem = {
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
};
|
||||
|
||||
describe('<IntegrationCard />', () => {
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: { http: { basePath: { prepend: jest.fn() } } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the card with skeleton while loading last activity', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<IntegrationCard
|
||||
data-test-subj={dataTestSubj}
|
||||
integration={integration}
|
||||
isLoading={true}
|
||||
lastActivity={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByTestId(dataTestSubj)).toHaveTextContent('Splunk');
|
||||
expect(
|
||||
getByTestId(`${dataTestSubj}${LAST_ACTIVITY_LOADING_SKELETON_TEST_ID}`)
|
||||
).toBeInTheDocument();
|
||||
expect(queryByTestId(`${dataTestSubj}${LAST_ACTIVITY_VALUE_TEST_ID}`)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the card with last activity value', () => {
|
||||
const lastActivity = 1735711200000; // Wed Jan 01 2025 00:00:00 GMT-0600 (Central Standard Time)
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<IntegrationCard
|
||||
data-test-subj={dataTestSubj}
|
||||
integration={integration}
|
||||
isLoading={false}
|
||||
lastActivity={lastActivity}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
queryByTestId(`${dataTestSubj}${LAST_ACTIVITY_LOADING_SKELETON_TEST_ID}`)
|
||||
).not.toBeInTheDocument();
|
||||
expect(getByTestId(`${dataTestSubj}${LAST_ACTIVITY_VALUE_TEST_ID}`)).toHaveTextContent(
|
||||
'Last synced: 2025-01-01T06:00:00Z'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSkeletonText,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { CardIcon } from '@kbn/fleet-plugin/public';
|
||||
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||
|
||||
const LAST_SYNCED = i18n.translate(
|
||||
'xpack.securitySolution.alertSummary.integrations.lastSyncedLabel',
|
||||
{
|
||||
defaultMessage: 'Last synced: ',
|
||||
}
|
||||
);
|
||||
|
||||
const MIN_WIDTH = 200;
|
||||
|
||||
export const LAST_ACTIVITY_LOADING_SKELETON_TEST_ID = '-last-activity-loading-skeleton';
|
||||
export const LAST_ACTIVITY_VALUE_TEST_ID = '-last-activity-value';
|
||||
|
||||
export interface IntegrationProps {
|
||||
/**
|
||||
* Installed AI for SOC integration
|
||||
*/
|
||||
integration: PackageListItem;
|
||||
/**
|
||||
* True while retrieving data streams to provide the last activity value
|
||||
*/
|
||||
isLoading: boolean;
|
||||
/**
|
||||
* Timestamp of the last time the integration synced (via data streams)
|
||||
*/
|
||||
lastActivity: number | undefined;
|
||||
/**
|
||||
* Data test subject string for testing
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendered on the alert summary page. The card displays the icon, name and last sync value.
|
||||
*/
|
||||
export const IntegrationCard = memo(
|
||||
({ 'data-test-subj': dataTestSubj, integration, isLoading, lastActivity }: IntegrationProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
css={css`
|
||||
min-width: ${MIN_WIDTH}px;
|
||||
`}
|
||||
data-test-subj={dataTestSubj}
|
||||
hasBorder={true}
|
||||
hasShadow={false}
|
||||
paddingSize="s"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<CardIcon
|
||||
icons={integration.icons}
|
||||
integrationName={integration.title}
|
||||
packageName={integration.name}
|
||||
size="xl"
|
||||
version={integration.version}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiText
|
||||
css={css`
|
||||
font-weight: ${euiTheme.font.weight.medium};
|
||||
`}
|
||||
size="xs"
|
||||
>
|
||||
{integration.title}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSkeletonText
|
||||
data-test-subj={`${dataTestSubj}${LAST_ACTIVITY_LOADING_SKELETON_TEST_ID}`}
|
||||
isLoading={isLoading}
|
||||
lines={1}
|
||||
size="xs"
|
||||
>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
data-test-subj={`${dataTestSubj}${LAST_ACTIVITY_VALUE_TEST_ID}`}
|
||||
size="xs"
|
||||
>
|
||||
{LAST_SYNCED}
|
||||
<FormattedRelativePreferenceDate value={lastActivity} />
|
||||
</EuiText>
|
||||
</EuiSkeletonText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IntegrationCard.displayName = 'IntegrationCard';
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import {
|
||||
ADD_INTEGRATIONS_BUTTON_TEST_ID,
|
||||
CARD_TEST_ID,
|
||||
IntegrationSection,
|
||||
} from './integration_section';
|
||||
import { useAddIntegrationsUrl } from '../../../../common/hooks/use_add_integrations_url';
|
||||
import { useIntegrationsLastActivity } from '../../../hooks/alert_summary/use_integrations_last_activity';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
jest.mock('../../../../common/hooks/use_add_integrations_url');
|
||||
jest.mock('../../../hooks/alert_summary/use_integrations_last_activity');
|
||||
jest.mock('@kbn/kibana-react-plugin/public');
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
name: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
status: installationStatuses.Installed,
|
||||
title: 'Splunk',
|
||||
version: '',
|
||||
},
|
||||
{
|
||||
id: 'google_secops',
|
||||
name: 'google_secops',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
status: installationStatuses.Installed,
|
||||
title: 'Google SecOps',
|
||||
version: '',
|
||||
},
|
||||
];
|
||||
|
||||
describe('<IntegrationSection />', () => {
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: { navigateToApp: jest.fn() },
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn().mockReturnValue('/app/integrations/detail/splunk-0.1.0/overview'),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a card for each integration ', () => {
|
||||
(useAddIntegrationsUrl as jest.Mock).mockReturnValue({ onClick: jest.fn() });
|
||||
(useIntegrationsLastActivity as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
lastActivities: {},
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<IntegrationSection packages={packages} />);
|
||||
|
||||
expect(getByTestId(`${CARD_TEST_ID}splunk`)).toHaveTextContent('Splunk');
|
||||
expect(getByTestId(`${CARD_TEST_ID}google_secops`)).toHaveTextContent('Google SecOps');
|
||||
});
|
||||
|
||||
it('should navigate to the fleet page when clicking on the add integrations button', () => {
|
||||
const addIntegration = jest.fn();
|
||||
(useAddIntegrationsUrl as jest.Mock).mockReturnValue({
|
||||
onClick: addIntegration,
|
||||
});
|
||||
(useIntegrationsLastActivity as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const { getByTestId } = render(<IntegrationSection packages={[]} />);
|
||||
|
||||
getByTestId(ADD_INTEGRATIONS_BUTTON_TEST_ID).click();
|
||||
|
||||
expect(addIntegration).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { useIntegrationsLastActivity } from '../../../hooks/alert_summary/use_integrations_last_activity';
|
||||
import { IntegrationCard } from './integration_card';
|
||||
import { useAddIntegrationsUrl } from '../../../../common/hooks/use_add_integrations_url';
|
||||
|
||||
const ADD_INTEGRATION = i18n.translate(
|
||||
'xpack.securitySolution.alertSummary.integrations.addIntegrationButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add integration',
|
||||
}
|
||||
);
|
||||
|
||||
export const CARD_TEST_ID = 'alert-summary-integration-card-';
|
||||
export const ADD_INTEGRATIONS_BUTTON_TEST_ID = 'alert-summary-add-integrations-button';
|
||||
|
||||
export interface IntegrationSectionProps {
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Section rendered at the top of the alert summary page. It displays all the AI for SOC installed integrations
|
||||
* and allow the user to add more integrations by clicking on a button that links to a Fleet page.
|
||||
* Each integration card is also displaying the last time the sync happened (using streams).
|
||||
*/
|
||||
export const IntegrationSection = memo(({ packages }: IntegrationSectionProps) => {
|
||||
const { onClick: addIntegration } = useAddIntegrationsUrl(); // TODO this link might have to be revisited once the integration work is done
|
||||
const { isLoading, lastActivities } = useIntegrationsLastActivity({ packages });
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center" wrap>
|
||||
{packages.map((pkg) => (
|
||||
<EuiFlexItem grow={false} key={pkg.name}>
|
||||
<IntegrationCard
|
||||
data-test-subj={`${CARD_TEST_ID}${pkg.name}`}
|
||||
integration={pkg}
|
||||
isLoading={isLoading}
|
||||
lastActivity={lastActivities[pkg.name]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj={ADD_INTEGRATIONS_BUTTON_TEST_ID}
|
||||
iconType="plusInCircle"
|
||||
onClick={addIntegration}
|
||||
>
|
||||
{ADD_INTEGRATION}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
||||
IntegrationSection.displayName = 'IntegrationSection';
|
|
@ -18,6 +18,9 @@ import {
|
|||
} from './wrapper';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url';
|
||||
import { useIntegrationsLastActivity } from '../../hooks/alert_summary/use_integrations_last_activity';
|
||||
import { ADD_INTEGRATIONS_BUTTON_TEST_ID } from './integrations/integration_section';
|
||||
import { SEARCH_BAR_TEST_ID } from './search_bar/search_bar_section';
|
||||
import { KPIS_SECTION } from './kpis/kpis_section';
|
||||
|
||||
|
@ -26,6 +29,8 @@ jest.mock('../../../common/components/search_bar', () => ({
|
|||
SiemSearchBar: () => <div data-test-subj={'alert-summary-search-bar'} />,
|
||||
}));
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../../common/hooks/use_add_integrations_url');
|
||||
jest.mock('../../hooks/alert_summary/use_integrations_last_activity');
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
|
@ -47,6 +52,7 @@ describe('<Wrapper />', () => {
|
|||
clearInstanceCache: jest.fn(),
|
||||
},
|
||||
},
|
||||
http: { basePath: { prepend: jest.fn() } },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -86,6 +92,11 @@ describe('<Wrapper />', () => {
|
|||
});
|
||||
|
||||
it('should render the content if the dataView is created correctly', async () => {
|
||||
(useAddIntegrationsUrl as jest.Mock).mockReturnValue({ onClick: jest.fn() });
|
||||
(useIntegrationsLastActivity as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
lastActivities: {},
|
||||
});
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
data: {
|
||||
|
@ -116,6 +127,7 @@ describe('<Wrapper />', () => {
|
|||
|
||||
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(CONTENT_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(ADD_INTEGRATIONS_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(KPIS_SECTION)).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
|
|||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { KPIsSection } from './kpis/kpis_section';
|
||||
import { IntegrationSection } from './integrations/integration_section';
|
||||
import { SearchBarSection } from './search_bar/search_bar_section';
|
||||
|
||||
const DATAVIEW_ERROR = i18n.translate('xpack.securitySolution.alertSummary.dataViewError', {
|
||||
|
@ -92,6 +93,8 @@ export const Wrapper = memo(({ packages }: WrapperProps) => {
|
|||
/>
|
||||
) : (
|
||||
<div data-test-subj={CONTENT_TEST_ID}>
|
||||
<IntegrationSection packages={packages} />
|
||||
<EuiHorizontalRule />
|
||||
<SearchBarSection dataView={dataView} packages={packages} />
|
||||
<EuiSpacer />
|
||||
<KPIsSection dataView={dataView} />
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { installationStatuses, useGetDataStreams } from '@kbn/fleet-plugin/public';
|
||||
import { useIntegrationsLastActivity } from './use_integrations_last_activity';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
|
||||
jest.mock('@kbn/fleet-plugin/public');
|
||||
|
||||
const oldestLastActivity = 1735711200000;
|
||||
const newestLastActivity = oldestLastActivity + 1000;
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
name: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
status: installationStatuses.Installed,
|
||||
title: 'Splunk',
|
||||
version: '',
|
||||
},
|
||||
{
|
||||
id: 'google_secops',
|
||||
name: 'google_secops',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
status: installationStatuses.Installed,
|
||||
title: 'Google SecOps',
|
||||
version: '',
|
||||
},
|
||||
];
|
||||
|
||||
describe('useIntegrationsLastActivity', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return isLoading true', () => {
|
||||
(useGetDataStreams as jest.Mock).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIntegrationsLastActivity({ packages }));
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.lastActivities).toEqual({});
|
||||
});
|
||||
|
||||
it('should return an object with package name and last sync values', () => {
|
||||
(useGetDataStreams as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data_streams: [{ package: 'splunk', last_activity_ms: oldestLastActivity }],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIntegrationsLastActivity({ packages }));
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.lastActivities.splunk).toBe(oldestLastActivity);
|
||||
});
|
||||
|
||||
it('should return most recent value for integration matching multiple dataStreams', () => {
|
||||
(useGetDataStreams as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data_streams: [
|
||||
{ package: 'splunk', last_activity_ms: oldestLastActivity },
|
||||
{ package: 'splunk', last_activity_ms: newestLastActivity },
|
||||
{ package: 'google_secops', last_activity_ms: oldestLastActivity },
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIntegrationsLastActivity({ packages }));
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.lastActivities.splunk).toBe(newestLastActivity);
|
||||
expect(result.current.lastActivities.google_secops).toBe(oldestLastActivity);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { DataStream, PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { useGetDataStreams } from '@kbn/fleet-plugin/public';
|
||||
|
||||
export interface UseIntegrationsLastActivityParams {
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
}
|
||||
|
||||
export interface UseIntegrationsLastActivityResult {
|
||||
/**
|
||||
* Is true while the data is loading
|
||||
*/
|
||||
isLoading: boolean;
|
||||
/**
|
||||
* Object that stores each integration name/last activity values
|
||||
*/
|
||||
lastActivities: { [id: string]: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches dataStreams, finds all the dataStreams for each integration, takes the value of the latest updated stream.
|
||||
* Returns an object with the package name as the key and the last time it was synced (using data streams) as the value.
|
||||
*/
|
||||
export const useIntegrationsLastActivity = ({
|
||||
packages,
|
||||
}: UseIntegrationsLastActivityParams): UseIntegrationsLastActivityResult => {
|
||||
const { data, isLoading } = useGetDataStreams();
|
||||
|
||||
// Find all the matching dataStreams for our packages, take the most recently updated one for each package.
|
||||
const lastActivities: { [id: string]: number } = useMemo(() => {
|
||||
const la: { [id: string]: number } = {};
|
||||
packages.forEach((p: PackageListItem) => {
|
||||
const dataStreams = (data?.data_streams || []).filter(
|
||||
(d: DataStream) => d.package === p.name
|
||||
);
|
||||
dataStreams.sort((a, b) => b.last_activity_ms - a.last_activity_ms);
|
||||
const lastActivity = dataStreams.shift();
|
||||
|
||||
if (lastActivity) {
|
||||
la[p.name] = lastActivity.last_activity_ms;
|
||||
}
|
||||
});
|
||||
return la;
|
||||
}, [data, packages]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isLoading,
|
||||
lastActivities,
|
||||
}),
|
||||
[isLoading, lastActivities]
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue