Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c58d4e2
[AI4DSOC] Alert summary page routing and initialization (#214889)
PhilippeOberti Mar 20, 2025
c8027c9
[AI4DSOC] Alert summary landing page (#215246)
PhilippeOberti Mar 21, 2025
0625a0c
[AI4DSOC] Alert summary dataview (#215265)
PhilippeOberti Mar 21, 2025
cba22a7
[AI4DSOC] Alert summary KQL bar (#215586)
PhilippeOberti Mar 28, 2025
6e7f6bf
[AI4DSOC] Alert summary KPI charts (#215585)
PhilippeOberti Mar 28, 2025
efd4ffe
[AI4DSOR] Alert summary integrations section (#215266)
PhilippeOberti Apr 3, 2025
703c1bd
[AI4DSOC] Fix issue with filtering by integrations (#216574)
PhilippeOberti Apr 4, 2025
710e987
[AI4DSOC] Alert summary table setup (#216744)
PhilippeOberti Apr 10, 2025
2ca805d
Alerty summary table flyout setup (#217421)
PhilippeOberti Apr 10, 2025
5597ff9
[AI4DSOC] Alert summary alert actions in table and flyout (#217696)
PhilippeOberti Apr 14, 2025
f446b07
[AI4DSOC] Alert summary table custom cell renderers (#217124)
PhilippeOberti Apr 17, 2025
e15a66e
[AI4DSOC] Alert summary table and flyout ai assistant (#217744)
PhilippeOberti Apr 18, 2025
45032f0
[AI4DSOC] Alert summary page performance improvements (#218632)
PhilippeOberti Apr 18, 2025
20cb27d
[AI4DSOC] Change the Attack Discovery page to use the AI for SOC aler…
PhilippeOberti Apr 21, 2025
b330373
[AI4DSOC] Change the Cases page to use the AI for SOC alerts table (#…
PhilippeOberti Apr 21, 2025
3002358
[AI4DSOC] Fix spacing issue on alert summary landing page integration…
PhilippeOberti Apr 22, 2025
59d5d00
[AI4DSOC][ResponseOps] Fix alerts table not handling undefined mainte…
PhilippeOberti Apr 23, 2025
5d36412
[AI4DSOC] Fix link to the new integrations page (#219030)
PhilippeOberti Apr 24, 2025
014978a
[AI4DSOC] Disable CellActions and PreviewLinks on the Attack discover…
PhilippeOberti Apr 24, 2025
66bb5e9
[AI4DSOC] Add cell renderer for datetime fields to the alert summary …
PhilippeOberti Apr 24, 2025
2ceb198
[AI4DSOC] Remove Assistant icon from row action in alert summary tabl…
PhilippeOberti Apr 24, 2025
27a9cf4
[AI4DSOC] Add checkboxes to the alert summary table (#219169)
PhilippeOberti Apr 25, 2025
1719c8a
[Security Solution][AI4DSOC] Fix table not applying alert tags for At…
PhilippeOberti Apr 28, 2025
4531716
[AI4DSOC] Fix logic that renders the group title when grouping by int…
PhilippeOberti Apr 28, 2025
0804e9e
[AI4DSOC] Alert summary table truncates long values and display the f…
PhilippeOberti Apr 28, 2025
64977d3
[Security Solution] Fix alerts table potentially not applying alert a…
PhilippeOberti May 1, 2025
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
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { waitFor, renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common';
import * as api from '../apis/bulk_get_maintenance_windows';
import { coreMock } from '@kbn/core/public/mocks';
Expand Down Expand Up @@ -74,16 +74,17 @@ describe('useBulkGetMaintenanceWindowsQuery', () => {
beforeEach(async () => {
jest.clearAllMocks();
addErrorMock = notifications.toasts.addError as jest.Mock;
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });
});

it('calls the api when invoked with the correct parameters', async () => {
application.capabilities = {
...application.capabilities,
maintenanceWindow: {
show: true,
},
};
useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => true });
});

it('calls the api when invoked with the correct parameters', async () => {
const spy = jest.spyOn(api, 'bulkGetMaintenanceWindows');
spy.mockResolvedValue(response);

Expand All @@ -110,6 +111,13 @@ describe('useBulkGetMaintenanceWindowsQuery', () => {
});

it('does not call the api if the canFetchMaintenanceWindows is false', async () => {
application.capabilities = {
...application.capabilities,
maintenanceWindow: {
show: true,
},
};

const spy = jest.spyOn(api, 'bulkGetMaintenanceWindows');
spy.mockResolvedValue(response);

Expand All @@ -136,6 +144,13 @@ describe('useBulkGetMaintenanceWindowsQuery', () => {
});

it('does not call the api if license is not platinum', async () => {
application.capabilities = {
...application.capabilities,
maintenanceWindow: {
show: true,
},
};

useLicenseMock.mockReturnValue({ isAtLeastPlatinum: () => false });

const spy = jest.spyOn(api, 'bulkGetMaintenanceWindows');
Expand Down Expand Up @@ -186,7 +201,35 @@ describe('useBulkGetMaintenanceWindowsQuery', () => {
await waitFor(() => expect(spy).not.toHaveBeenCalled());
});

it('does not call the api if the maintenanceWindow capability is disabled', async () => {
const spy = jest.spyOn(api, 'bulkGetMaintenanceWindows');
spy.mockResolvedValue(response);

renderHook(
() =>
useBulkGetMaintenanceWindowsQuery({
ids: ['test-id'],
http,
notifications,
application,
licensing,
}),
{
wrapper,
}
);

await waitFor(() => expect(spy).not.toHaveBeenCalled());
});

it('shows a toast error when the api return an error', async () => {
application.capabilities = {
...application.capabilities,
maintenanceWindow: {
show: true,
},
};

const spy = jest
.spyOn(api, 'bulkGetMaintenanceWindows')
.mockRejectedValue(new Error('An error'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,7 @@ export const useBulkGetMaintenanceWindowsQuery = (
ids,
http,
notifications: { toasts },
application: {
capabilities: {
maintenanceWindow: { show },
},
},
application: { capabilities },
licensing,
}: UseBulkGetMaintenanceWindowsQueryParams,
{
Expand All @@ -70,6 +66,9 @@ export const useBulkGetMaintenanceWindowsQuery = (
const { isAtLeastPlatinum } = useLicense({ licensing });
const hasLicense = isAtLeastPlatinum();

// In AI4DSOC (searchAiLake tier) the maintenanceWindow capability is disabled
const show = Boolean(capabilities.maintenanceWindow?.show);

return useQuery({
queryKey: queryKeys.maintenanceWindowsBulkGet(ids),
queryFn: () => bulkGetMaintenanceWindows({ http, ids }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,30 @@ describe('ContextPills', () => {
});
});

it('does not render item if description is empty', () => {
render(
<TestProviders>
<ContextPills
{...defaultProps}
promptContexts={{
...mockPromptContexts,
context3: {
category: 'event',
description: '',
getPromptContext: () => Promise.resolve('Context 2 data'),
id: 'context3',
tooltip: 'Context 2 tooltip',
},
}}
selectedPromptContexts={{}}
setSelectedPromptContexts={jest.fn()}
/>
</TestProviders>
);
expect(screen.getByTestId(`pillButton-context2`)).toBeInTheDocument();
expect(screen.queryByTestId(`pillButton-context3`)).not.toBeInTheDocument();
});

it('invokes setSelectedPromptContexts() when the prompt is NOT already selected', async () => {
const context = mockPromptContexts.context1;
const setSelectedPromptContexts = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ const ContextPillsComponent: React.FC<Props> = ({
{description}
</EuiButtonEmpty>
);
return (
return description.length > 0 ? (
<EuiFlexItem grow={false} key={id}>
{selectedPromptContexts[id] != null ? (
button
) : (
<EuiToolTip content={tooltip}>{button}</EuiToolTip>
)}
</EuiFlexItem>
);
) : null;
})}
</EuiFlexGroup>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { NewChatByTitle } from '.';
import { BUTTON_ICON_TEST_ID, BUTTON_TEST_ID, BUTTON_TEXT_TEST_ID, NewChatByTitle } from '.';

const testProps = {
showAssistantOverlay: jest.fn(),
Expand All @@ -20,60 +20,28 @@ describe('NewChatByTitle', () => {
jest.clearAllMocks();
});

it('renders the default New Chat button with a discuss icon', () => {
render(<NewChatByTitle {...testProps} />);
it('should render icon only by default', () => {
const { getByTestId, queryByTestId } = render(<NewChatByTitle {...testProps} />);

const newChatButton = screen.getByTestId('newChatByTitle');

expect(newChatButton.querySelector('[data-euiicon-type="discuss"]')).toBeInTheDocument();
});

it('renders the default "New Chat" text when children are NOT provided', () => {
render(<NewChatByTitle {...testProps} />);

const newChatButton = screen.getByTestId('newChatByTitle');

expect(newChatButton.textContent).toContain('Chat');
expect(getByTestId(BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(BUTTON_TEXT_TEST_ID)).not.toBeInTheDocument();
});

it('renders custom children', async () => {
render(<NewChatByTitle {...testProps}>{'🪄✨'}</NewChatByTitle>);

const newChatButton = screen.getByTestId('newChatByTitle');

expect(newChatButton.textContent).toContain('🪄✨');
});

it('renders custom icons', async () => {
render(<NewChatByTitle {...testProps} iconType="help" />);

const newChatButton = screen.getByTestId('newChatByTitle');
it('should render the button with icon and text', () => {
const { getByTestId } = render(<NewChatByTitle {...testProps} text={'Ask AI Assistant'} />);

expect(newChatButton.querySelector('[data-euiicon-type="help"]')).toBeInTheDocument();
});

it('does NOT render an icon when iconType is null', () => {
render(<NewChatByTitle {...testProps} iconType={null} />);

const newChatButton = screen.getByTestId('newChatByTitle');

expect(newChatButton.querySelector('.euiButtonContent__icon')).not.toBeInTheDocument();
});

it('renders button icon when iconOnly is true', async () => {
render(<NewChatByTitle {...testProps} iconOnly />);

const newChatButton = screen.getByTestId('newChatByTitle');

expect(newChatButton.querySelector('[data-euiicon-type="discuss"]')).toBeInTheDocument();
expect(newChatButton.textContent).not.toContain('Chat');
expect(getByTestId(BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(BUTTON_ICON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(BUTTON_TEXT_TEST_ID)).toHaveTextContent('Ask AI Assistant');
});

it('calls showAssistantOverlay on click', async () => {
render(<NewChatByTitle {...testProps} />);
const newChatButton = screen.getByTestId('newChatByTitle');
const { getByTestId } = render(<NewChatByTitle {...testProps} />);

const button = getByTestId(BUTTON_TEST_ID);

await userEvent.click(newChatButton);
await userEvent.click(button);

expect(testProps.showAssistantOverlay).toHaveBeenCalledWith(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,76 +5,78 @@
* 2.0.
*/

import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';

import type { EuiButtonColor } from '@elastic/eui';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback } from 'react';
import { AssistantIcon } from '@kbn/ai-assistant-icon';
import { EuiButtonEmptySizes } from '@elastic/eui/src/components/button/button_empty/button_empty';
import * as i18n from './translations';

export interface Props {
children?: React.ReactNode;
/** Defaults to `discuss`. If null, the button will not have an icon */
iconType?: string | null;
export const BUTTON_TEST_ID = 'newChatByTitle';
export const BUTTON_ICON_TEST_ID = 'newChatByTitleIcon';
export const BUTTON_TEXT_TEST_ID = 'newChatByTitleText';

export interface NewChatByTitleComponentProps {
/**
* Optionally specify color of empty button.
* @default 'primary'
*/
color?: EuiButtonColor;
/**
* Callback to display the assistant overlay
*/
showAssistantOverlay: (show: boolean) => void;
/** Defaults to false. If true, shows icon button without text */
iconOnly?: boolean;
/**
*
*/
size?: EuiButtonEmptySizes;
/**
* Optionally specify the text to display.
*/
text?: string;
}

const NewChatByTitleComponent: React.FC<Props> = ({
children = i18n.NEW_CHAT,
iconType,
const NewChatByTitleComponent: React.FC<NewChatByTitleComponentProps> = ({
color = 'primary',
showAssistantOverlay,
iconOnly = false,
size = 'm',
text,
}) => {
const showOverlay = useCallback(() => {
showAssistantOverlay(true);
}, [showAssistantOverlay]);

const icon = useMemo(() => {
if (iconType === null) {
return undefined;
}

return iconType ?? 'discuss';
}, [iconType]);
const showOverlay = useCallback(() => showAssistantOverlay(true), [showAssistantOverlay]);

return useMemo(
() =>
iconOnly ? (
<EuiToolTip content={i18n.NEW_CHAT}>
<EuiButtonIcon
data-test-subj="newChatByTitle"
iconType={icon ?? 'discuss'}
onClick={showOverlay}
color={'text'}
aria-label={i18n.NEW_CHAT}
/>
</EuiToolTip>
) : (
<EuiButtonEmpty
data-test-subj="newChatByTitle"
iconType={icon}
onClick={showOverlay}
aria-label={i18n.NEW_CHAT}
>
{children}
</EuiButtonEmpty>
),
[children, icon, showOverlay, iconOnly]
return (
<EuiButtonEmpty
aria-label={i18n.ASK_AI_ASSISTANT}
color={color}
data-test-subj={BUTTON_TEST_ID}
onClick={showOverlay}
size={size}
>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<AssistantIcon data-test-subj={BUTTON_ICON_TEST_ID} size="m" />
</EuiFlexItem>
{text && (
<EuiFlexItem data-test-subj={BUTTON_TEXT_TEST_ID} grow={false}>
{text}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiButtonEmpty>
);
};

NewChatByTitleComponent.displayName = 'NewChatByTitleComponent';

/**
* `NewChatByTitle` displays a _New chat_ icon button by providing only the `promptContextId`
* `NewChatByTitle` displays a button by providing only the `promptContextId`
* of a context that was (already) registered by the `useAssistantOverlay` hook. You may
* optionally style the button icon, or override the default _New chat_ text with custom
* content, like {'🪄✨'}
* optionally override the default text.
*
* USE THIS WHEN: all the data necessary to start a new chat is NOT available
* in the same part of the React tree as the _New chat_ button. When paired
* with the `useAssistantOverlay` hook, this option enables context to be be
* registered where the data is available, and then the _New chat_ button can be displayed
* in the same part of the React tree as the button. When paired
* with the `useAssistantOverlay` hook, this option enables context to be
* registered where the data is available, and then the button can be displayed
* in another part of the tree.
*/
export const NewChatByTitle = React.memo(NewChatByTitleComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import { i18n } from '@kbn/i18n';

export const NEW_CHAT = i18n.translate(
export const ASK_AI_ASSISTANT = i18n.translate(
'xpack.elasticAssistant.assistant.newChatByTitle.newChatByTitleButton',
{
defaultMessage: 'Chat',
defaultMessage: 'Ask AI Assistant',
}
);
Loading
Loading