Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { esqlResponseToRecords } from './esql';

describe('esqlResponseToRecords', () => {
it('returns an empty array if the response is undefined', () => {
const result = esqlResponseToRecords(undefined);
expect(result).toEqual([]);
});

it('converts ESQL response to records', () => {
const response = {
columns: [
{ name: 'field1', type: '' },
{ name: 'field2', type: '' },
],
values: [
['value1', 'value2'],
['value3', 'value4'],
],
};
const result = esqlResponseToRecords(response);
expect(result).toEqual([
{ field1: 'value1', field2: 'value2' },
{ field1: 'value3', field2: 'value4' },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 type { ESQLSearchResponse } from '@kbn/es-types';

// Function copied from elasticsearch-8.x/lib/helpers
export function esqlResponseToRecords<TDocument extends Record<string, unknown>>(
response: ESQLSearchResponse | undefined
): TDocument[] {
if (!response) return [];
const { columns, values } = response;
return values.map<TDocument>((row) => {
const doc: Record<string, unknown> = {};
row.forEach((cell, index) => {
const { name } = columns[index];
doc[name] = cell;
});
return doc as TDocument;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
LAST_ACTIVITY_VALUE_TEST_ID,
} from './integration_card';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useIntegrationLastAlertIngested } from '../../../hooks/alert_summary/use_integration_last_alert_ingested';

jest.mock('@kbn/kibana-react-plugin/public');
jest.mock('../../../hooks/alert_summary/use_integration_last_alert_ingested');

const dataTestSubj = 'test-id';
const integration: PackageListItem = {
Expand All @@ -39,13 +41,14 @@ describe('<IntegrationCard />', () => {
});

it('should render the card with skeleton while loading last activity', () => {
(useIntegrationLastAlertIngested as jest.Mock).mockReturnValue({
isLoading: true,
lastAlertIngested: null,
refetch: jest.fn(),
});

const { getByTestId, queryByTestId } = render(
<IntegrationCard
data-test-subj={dataTestSubj}
integration={integration}
isLoading={true}
lastActivity={undefined}
/>
<IntegrationCard data-test-subj={dataTestSubj} integration={integration} />
);

expect(getByTestId(dataTestSubj)).toHaveTextContent('Splunk');
Expand All @@ -56,14 +59,14 @@ describe('<IntegrationCard />', () => {
});

it('should render the card with last activity value', () => {
const lastActivity = 1735711200000; // Wed Jan 01 2025 00:00:00 GMT-0600 (Central Standard Time)
(useIntegrationLastAlertIngested as jest.Mock).mockReturnValue({
isLoading: false,
lastAlertIngested: 1735711200000, // Wed Jan 01 2025 00:00:00 GMT-0600 (Central Standard Time)
refetch: jest.fn(),
});

const { getByTestId, queryByTestId } = render(
<IntegrationCard
data-test-subj={dataTestSubj}
integration={integration}
isLoading={false}
lastActivity={lastActivity}
/>
<IntegrationCard data-test-subj={dataTestSubj} integration={integration} />
);

expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { memo } from 'react';
import React, { memo, useEffect } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -17,6 +17,7 @@ import {
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
import { useIntegrationLastAlertIngested } from '../../../hooks/alert_summary/use_integration_last_alert_ingested';
import { IntegrationIcon } from '../common/integration_icon';
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';

Expand All @@ -28,6 +29,7 @@ const LAST_SYNCED = i18n.translate(
);

const MIN_WIDTH = 200;
const REFRESH_INTERVAL = 30000; // 30 seconds

export const LAST_ACTIVITY_LOADING_SKELETON_TEST_ID = '-last-activity-loading-skeleton';
export const LAST_ACTIVITY_VALUE_TEST_ID = '-last-activity-value';
Expand All @@ -37,27 +39,30 @@ 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.
* Rendered on the alert summary page. The card displays the icon, name and last time the integration received alert data.
*/
export const IntegrationCard = memo(
({ 'data-test-subj': dataTestSubj, integration, isLoading, lastActivity }: IntegrationProps) => {
({ 'data-test-subj': dataTestSubj, integration }: IntegrationProps) => {
const { euiTheme } = useEuiTheme();

const { isLoading, lastAlertIngested, refetch } = useIntegrationLastAlertIngested({
integrationName: integration.name,
});

// force a refresh every 30 seconds to update the last activity time
useEffect(() => {
const interval = setInterval(() => refetch(), REFRESH_INTERVAL);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<EuiPanel
css={css`
Expand Down Expand Up @@ -101,7 +106,11 @@ export const IntegrationCard = memo(
size="xs"
>
{LAST_SYNCED}
<FormattedRelativePreferenceDate value={lastActivity} />
<FormattedRelativePreferenceDate
value={lastAlertIngested}
/* we use key here to force re-render of the relative time */
key={Date.now()}
/>
</EuiText>
</EuiSkeletonText>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import {
CARD_TEST_ID,
IntegrationSection,
} from './integration_section';
import { useIntegrationsLastActivity } from '../../../hooks/alert_summary/use_integrations_last_activity';
import { useIntegrationLastAlertIngested } from '../../../hooks/alert_summary/use_integration_last_alert_ingested';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useNavigateToIntegrationsPage } from '../../../hooks/alert_summary/use_navigate_to_integrations_page';

jest.mock('../../../hooks/alert_summary/use_navigate_to_integrations_page');
jest.mock('../../../hooks/alert_summary/use_integrations_last_activity');
jest.mock('../../../hooks/alert_summary/use_integration_last_alert_ingested');
jest.mock('@kbn/kibana-react-plugin/public');

const packages: PackageListItem[] = [
Expand Down Expand Up @@ -62,9 +62,9 @@ describe('<IntegrationSection />', () => {

it('should render a card for each integration ', () => {
(useNavigateToIntegrationsPage as jest.Mock).mockReturnValue(jest.fn());
(useIntegrationsLastActivity as jest.Mock).mockReturnValue({
(useIntegrationLastAlertIngested as jest.Mock).mockReturnValue({
isLoading: true,
lastActivities: {},
lastAlertIngested: {},
});

const { getByTestId } = render(<IntegrationSection packages={packages} />);
Expand All @@ -76,7 +76,7 @@ describe('<IntegrationSection />', () => {
it('should navigate to the fleet page when clicking on the add integrations button', () => {
const navigateToIntegrationsPage = jest.fn();
(useNavigateToIntegrationsPage as jest.Mock).mockReturnValue(navigateToIntegrationsPage);
(useIntegrationsLastActivity as jest.Mock).mockReturnValue([]);
(useIntegrationLastAlertIngested as jest.Mock).mockReturnValue([]);

const { getByTestId } = render(<IntegrationSection packages={[]} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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 { useNavigateToIntegrationsPage } from '../../../hooks/alert_summary/use_navigate_to_integrations_page';

Expand Down Expand Up @@ -37,20 +36,14 @@ export interface IntegrationSectionProps {
*/
export const IntegrationSection = memo(({ packages }: IntegrationSectionProps) => {
const navigateToIntegrationsPage = useNavigateToIntegrationsPage();
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]}
/>
<IntegrationCard data-test-subj={`${CARD_TEST_ID}${pkg.name}`} integration={pkg} />
</EuiFlexItem>
))}
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
Wrapper,
} from './wrapper';
import { TestProviders } from '../../../common/mock';
import { useIntegrationsLastActivity } from '../../hooks/alert_summary/use_integrations_last_activity';
import { useIntegrationLastAlertIngested } from '../../hooks/alert_summary/use_integration_last_alert_ingested';
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';
Expand All @@ -35,7 +35,7 @@ jest.mock('../alerts_table/alerts_grouping', () => ({
GroupedAlertsTable: () => <div />,
}));
jest.mock('../../hooks/alert_summary/use_navigate_to_integrations_page');
jest.mock('../../hooks/alert_summary/use_integrations_last_activity');
jest.mock('../../hooks/alert_summary/use_integration_last_alert_ingested');
jest.mock('../../../common/hooks/use_experimental_features');
jest.mock('../../../common/hooks/use_create_data_view');
jest.mock('../../../data_view_manager/hooks/use_data_view');
Expand Down Expand Up @@ -98,9 +98,9 @@ describe('<Wrapper />', () => {

it('should render the content if the dataView is created correctly', async () => {
(useNavigateToIntegrationsPage as jest.Mock).mockReturnValue(jest.fn());
(useIntegrationsLastActivity as jest.Mock).mockReturnValue({
(useIntegrationLastAlertIngested as jest.Mock).mockReturnValue({
isLoading: true,
lastActivities: {},
lastAlertIngested: {},
});
(useCreateDataView as jest.Mock).mockReturnValue({
dataView: { getIndexPattern: jest.fn(), id: 'id', toSpec: jest.fn() },
Expand Down Expand Up @@ -167,9 +167,9 @@ describe('<Wrapper />', () => {

it('should render the content if the dataView is created correctly', async () => {
(useNavigateToIntegrationsPage as jest.Mock).mockReturnValue(jest.fn());
(useIntegrationsLastActivity as jest.Mock).mockReturnValue({
(useIntegrationLastAlertIngested as jest.Mock).mockReturnValue({
isLoading: true,
lastActivities: {},
lastAlertIngested: {},
});
(useDataView as jest.Mock).mockReturnValue({
dataView: { getIndexPattern: jest.fn(), id: 'id', toSpec: jest.fn() },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { renderHook } from '@testing-library/react';
import { useIntegrationLastAlertIngested } from './use_integration_last_alert_ingested';
import { useQuery } from '@tanstack/react-query';

jest.mock('@tanstack/react-query');

const integrationName = 'splunk';

describe('useIntegrationLastAlertIngested', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should return isLoading true', () => {
(useQuery as jest.Mock).mockReturnValue({
isLoading: true,
data: undefined,
refetch: jest.fn(),
});

const { result } = renderHook(() => useIntegrationLastAlertIngested({ integrationName }));

expect(result.current.isLoading).toBe(true);
expect(result.current.lastAlertIngested).toEqual(null);
});

it('should return last AlertIngested', () => {
(useQuery as jest.Mock).mockReturnValue({
isLoading: false,
data: {
response: {
columns: [{ name: 'event.ingested' }],
values: [['2025-01-01T00:00:000Z']],
},
},
refetch: jest.fn(),
});

const { result } = renderHook(() => useIntegrationLastAlertIngested({ integrationName }));

expect(result.current.isLoading).toBe(false);
expect(result.current.lastAlertIngested).toEqual('2025-01-01T00:00:000Z');
});

it('should return refetch function', () => {
const refetch = jest.fn();
(useQuery as jest.Mock).mockReturnValue({
isLoading: false,
data: {},
refetch,
});

const { result } = renderHook(() => useIntegrationLastAlertIngested({ integrationName }));

expect(result.current.refetch).toBe(refetch);
});
});
Loading