[Security Solution] Fix AI Assistant overlay in Serverless pages (#166437)

## Summary

issue: https://github.com/elastic/kibana/issues/166436

![AI Assistant header
link](66614838-0e70-45b3-b79e-f4e70253fba2)

### Bug

The bug was caused by the `AssistantOverlay` being rendered inside the
`SecuritySolutionPageWrapper`, which is a "styling" wrapper that is not
used by all the pages in the Security Solution application, the
serverless-specific pages for instance, use their own page wrapper for
styling.

### Changes

#### Fix
This PR fixes the bug by rendering the `AssistantOverlay` in the
`HomePage`, along with other similar components such as the `HelpMenu`
or the `TopValuesPopover`, which are components that need to be always
present in the application layout.

#### Cleaning

The assistant provider configuration logic has also been moved from the
`App` component to a new `provider.tsx` component in the
_public/assistant_ directory, this way we don't pollute the main
Security `App` component with assistant-specific logic.

The `isAssistantEnabled` prop has been removed from the `Assistant`
component. We don't need to check the availability and pass it by props
everywhere we use the assistant, it can be retrieved from the assistant
context.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2023-09-18 12:45:14 +02:00 committed by GitHub
parent b2057ac148
commit 1dee16e061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 253 additions and 186 deletions

View file

@ -23,7 +23,7 @@ describe('AssistantOverlay', () => {
it('renders when isAssistantEnabled prop is true and keyboard shortcut is pressed', () => {
const { getByTestId } = render(
<TestProviders providerContext={{ assistantTelemetry }}>
<AssistantOverlay isAssistantEnabled={true} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
@ -34,7 +34,7 @@ describe('AssistantOverlay', () => {
it('modal closes when close button is clicked', () => {
const { getByLabelText, queryByTestId } = render(
<TestProviders>
<AssistantOverlay isAssistantEnabled={true} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
@ -47,7 +47,7 @@ describe('AssistantOverlay', () => {
it('Assistant invoked from shortcut tracking happens on modal open only (not close)', () => {
render(
<TestProviders providerContext={{ assistantTelemetry }}>
<AssistantOverlay isAssistantEnabled={true} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
@ -63,7 +63,7 @@ describe('AssistantOverlay', () => {
it('modal closes when shortcut is pressed and modal is already open', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isAssistantEnabled={true} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: ';', ctrlKey: true });
@ -75,7 +75,7 @@ describe('AssistantOverlay', () => {
it('modal does not open when incorrect shortcut is pressed', () => {
const { queryByTestId } = render(
<TestProviders>
<AssistantOverlay isAssistantEnabled={true} />
<AssistantOverlay />
</TestProviders>
);
fireEvent.keyDown(document, { key: 'a', ctrlKey: true });

View file

@ -22,15 +22,12 @@ const StyledEuiModal = styled(EuiModal)`
min-width: 95vw;
min-height: 25vh;
`;
interface Props {
isAssistantEnabled: boolean;
}
/**
* Modal container for Elastic AI Assistant conversations, receiving the page contents as context, plus whatever
* component currently has focus and any specific context it may provide through the SAssInterface.
*/
export const AssistantOverlay = React.memo<Props>(({ isAssistantEnabled }) => {
export const AssistantOverlay = React.memo(() => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [conversationId, setConversationId] = useState<string | undefined>(
WELCOME_CONVERSATION_TITLE
@ -103,11 +100,7 @@ export const AssistantOverlay = React.memo<Props>(({ isAssistantEnabled }) => {
<>
{isModalVisible && (
<StyledEuiModal onClose={handleCloseModal} data-test-subj="ai-assistant-modal">
<Assistant
isAssistantEnabled={isAssistantEnabled}
conversationId={conversationId}
promptContextId={promptContextId}
/>
<Assistant conversationId={conversationId} promptContextId={promptContextId} />
</StyledEuiModal>
)}
</>

View file

@ -22,7 +22,7 @@ import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
import { useLocalStorage } from 'react-use';
import { PromptEditor } from './prompt_editor';
import { QuickPrompts } from './quick_prompts/quick_prompts';
import { TestProviders } from '../mock/test_providers/test_providers';
import { mockAssistantAvailability, TestProviders } from '../mock/test_providers/test_providers';
jest.mock('../connectorland/use_load_connectors');
jest.mock('../connectorland/connector_setup');
@ -46,10 +46,10 @@ const getInitialConversations = (): Record<string, Conversation> => ({
},
});
const renderAssistant = (extraProps = {}) =>
const renderAssistant = (extraProps = {}, providerProps = {}) =>
render(
<TestProviders getInitialConversations={getInitialConversations}>
<Assistant isAssistantEnabled {...extraProps} />
<TestProviders getInitialConversations={getInitialConversations} {...providerProps}>
<Assistant {...extraProps} />
</TestProviders>
);
@ -110,9 +110,10 @@ describe('Assistant', () => {
data: connectors,
} as unknown as UseQueryResult<ActionConnector[], IHttpFetchError>);
const { getByLabelText } = render(
<TestProviders
getInitialConversations={() => ({
const { getByLabelText } = renderAssistant(
{},
{
getInitialConversations: () => ({
[WELCOME_CONVERSATION_TITLE]: {
id: WELCOME_CONVERSATION_TITLE,
messages: [],
@ -124,10 +125,8 @@ describe('Assistant', () => {
apiConfig: {},
excludeFromLastConversationStorage: true,
},
})}
>
<Assistant isAssistantEnabled />
</TestProviders>
}),
}
);
expect(persistToLocalStorage).toHaveBeenCalled();
@ -176,4 +175,16 @@ describe('Assistant', () => {
expect(persistToLocalStorage).toHaveBeenLastCalledWith(WELCOME_CONVERSATION_TITLE);
});
});
describe('when not authorized', () => {
it('should be disabled', async () => {
const { queryByTestId } = renderAssistant(
{},
{
assistantAvailability: { ...mockAssistantAvailability, isAssistantEnabled: false },
}
);
expect(queryByTestId('prompt-textarea')).toHaveProperty('disabled');
});
});
});

View file

@ -51,7 +51,6 @@ import { ConnectorMissingCallout } from '../connectorland/connector_missing_call
export interface Props {
conversationId?: string;
isAssistantEnabled: boolean;
promptContextId?: string;
shouldRefocusPrompt?: boolean;
showTitle?: boolean;
@ -64,7 +63,6 @@ export interface Props {
*/
const AssistantComponent: React.FC<Props> = ({
conversationId,
isAssistantEnabled,
promptContextId = '',
shouldRefocusPrompt = false,
showTitle = true,
@ -73,6 +71,7 @@ const AssistantComponent: React.FC<Props> = ({
const {
assistantTelemetry,
augmentMessageCodeBlocks,
assistantAvailability: { isAssistantEnabled },
conversations,
defaultAllow,
defaultAllowReplacement,

View file

@ -29,7 +29,7 @@ window.HTMLElement.prototype.scrollIntoView = jest.fn();
const mockGetInitialConversations = () => ({});
const mockAssistantAvailability: AssistantAvailability = {
export const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,

View file

@ -11,7 +11,7 @@
// happens in the root of your app. Optionally provide a custom title for the assistant:
/** provides context (from the app) to the assistant, and injects Kibana services, like `http` */
export { AssistantProvider } from './impl/assistant_context';
export { AssistantProvider, useAssistantContext } from './impl/assistant_context';
// Step 2: Add the `AssistantOverlay` component to your app. This component displays the assistant
// overlay in a modal, bound to a shortcut key:

View file

@ -5,10 +5,9 @@
* 2.0.
*/
import { AssistantProvider } from '@kbn/elastic-assistant';
import type { History } from 'history';
import type { FC } from 'react';
import React, { memo, useCallback } from 'react';
import React, { memo } from 'react';
import type { Store, Action } from 'redux';
import { Provider as ReduxStoreProvider } from 'react-redux';
@ -21,28 +20,18 @@ import { CellActionsProvider } from '@kbn/cell-actions';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { UpsellingProvider } from '../common/components/upselling_provider';
import { useAssistantTelemetry } from '../assistant/use_assistant_telemetry';
import { getComments } from '../assistant/get_comments';
import { augmentMessageCodeBlocks, LOCAL_STORAGE_KEY } from '../assistant/helpers';
import { useConversationStore } from '../assistant/use_conversation_store';
import { ManageUserInfo } from '../detections/components/user_info';
import { DEFAULT_DARK_MODE, APP_NAME, APP_ID } from '../../common/constants';
import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants';
import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher';
import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider';
import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters';
import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana';
import type { State } from '../common/store';
import { ASSISTANT_TITLE } from './translations';
import type { StartServices } from '../types';
import { PageRouter } from './routes';
import { UserPrivilegesProvider } from '../common/components/user_privileges/user_privileges_context';
import { ReactQueryClientProvider } from '../common/containers/query_client/query_client_provider';
import { DEFAULT_ALLOW, DEFAULT_ALLOW_REPLACEMENT } from '../assistant/content/anonymization';
import { PROMPT_CONTEXTS } from '../assistant/content/prompt_contexts';
import { BASE_SECURITY_QUICK_PROMPTS } from '../assistant/content/quick_prompts';
import { BASE_SECURITY_SYSTEM_PROMPTS } from '../assistant/content/prompts/system';
import { useAnonymizationStore } from '../assistant/use_anonymization_store';
import { useAssistantAvailability } from '../assistant/use_assistant_availability';
import { AssistantProvider } from '../assistant/provider';
interface StartAppComponent {
children: React.ReactNode;
@ -65,29 +54,12 @@ const StartAppComponent: FC<StartAppComponent> = ({
const {
i18n,
application: { capabilities },
http,
triggersActionsUi: { actionTypeRegistry },
uiActions,
upselling,
} = services;
const assistantAvailability = useAssistantAvailability();
const { conversations, setConversations } = useConversationStore();
const { defaultAllow, defaultAllowReplacement, setDefaultAllow, setDefaultAllowReplacement } =
useAnonymizationStore();
const getInitialConversation = useCallback(() => {
return conversations;
}, [conversations]);
const nameSpace = `${APP_ID}.${LOCAL_STORAGE_KEY}`;
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = useKibana().services.docLinks;
const assistantTelemetry = useAssistantTelemetry();
return (
<EuiErrorBoundary>
<i18n.Context>
@ -95,29 +67,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
<ReduxStoreProvider store={store}>
<KibanaThemeProvider theme$={theme$}>
<EuiThemeProvider darkMode={darkMode}>
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
augmentMessageCodeBlocks={augmentMessageCodeBlocks}
assistantAvailability={assistantAvailability}
assistantLangChain={false}
assistantTelemetry={assistantTelemetry}
defaultAllow={defaultAllow}
defaultAllowReplacement={defaultAllowReplacement}
docLinks={{ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }}
baseAllow={DEFAULT_ALLOW}
baseAllowReplacement={DEFAULT_ALLOW_REPLACEMENT}
basePromptContexts={Object.values(PROMPT_CONTEXTS)}
baseQuickPrompts={BASE_SECURITY_QUICK_PROMPTS}
baseSystemPrompts={BASE_SECURITY_SYSTEM_PROMPTS}
getInitialConversations={getInitialConversation}
getComments={getComments}
http={http}
nameSpace={nameSpace}
setConversations={setConversations}
setDefaultAllow={setDefaultAllow}
setDefaultAllowReplacement={setDefaultAllowReplacement}
title={ASSISTANT_TITLE}
>
<AssistantProvider>
<MlCapabilitiesProvider>
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
<ManageUserInfo>

View file

@ -26,7 +26,6 @@ import { TimelineId } from '../../../../common/types/timeline';
import { createStore } from '../../../common/store';
import { kibanaObservable } from '@kbn/timelines-plugin/public/mock';
import { sourcererPaths } from '../../../common/containers/sourcerer';
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
@ -49,15 +48,6 @@ jest.mock('react-reverse-portal', () => ({
createHtmlPortalNode: () => ({ unmount: jest.fn() }),
}));
jest.mock('../../../assistant/use_assistant_availability');
jest.mocked(useAssistantAvailability).mockReturnValue({
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
describe('global header', () => {
const mockSetHeaderActionMenu = jest.fn();
const state = {
@ -183,18 +173,11 @@ describe('global header', () => {
expect(queryByTestId('sourcerer-trigger')).not.toBeInTheDocument();
});
it('shows AI Assistant header link if user has necessary privileges', () => {
it('shows AI Assistant header link', () => {
(useLocation as jest.Mock).mockReturnValue([
{ pageName: SecurityPageName.overview, detailName: undefined },
]);
jest.mocked(useAssistantAvailability).mockReturnValue({
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
const { findByTestId } = render(
<TestProviders store={store}>
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
@ -203,25 +186,4 @@ describe('global header', () => {
waitFor(() => expect(findByTestId('assistantHeaderLink')).toBeInTheDocument());
});
it('does not show AI Assistant header link if user does not have necessary privileges', () => {
(useLocation as jest.Mock).mockReturnValue([
{ pageName: SecurityPageName.overview, detailName: undefined },
]);
jest.mocked(useAssistantAvailability).mockReturnValue({
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
const { findByTestId } = render(
<TestProviders store={store}>
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
</TestProviders>
);
waitFor(() => expect(findByTestId('assistantHeaderLink')).not.toBeInTheDocument());
});
});

View file

@ -27,8 +27,7 @@ import { timelineSelectors } from '../../../timelines/store/timeline';
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { getScopeFromPath, showSourcererByPath } from '../../../common/containers/sourcerer';
import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url';
import { AssistantHeaderLink } from './assistant_header_link';
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
import { AssistantHeaderLink } from '../../../assistant/header_link';
const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', {
defaultMessage: 'Add integrations',
@ -54,8 +53,6 @@ export const GlobalHeader = React.memo(
const { href, onClick } = useAddIntegrationsUrl();
const { hasAssistantPrivilege } = useAssistantAvailability();
useEffect(() => {
setHeaderActionMenu((element) => {
const mount = toMountPoint(<OutPortal node={portalNode} />, { theme$: theme.theme$ });
@ -91,7 +88,7 @@ export const GlobalHeader = React.memo(
{showSourcerer && !showTimeline && (
<Sourcerer scope={sourcererScope} data-test-subj="sourcerer" />
)}
{hasAssistantPrivilege && <AssistantHeaderLink />}
<AssistantHeaderLink />
</EuiHeaderLinks>
</EuiHeaderSectionItem>
</EuiHeaderSection>

View file

@ -29,6 +29,7 @@ import { useUpdateExecutionContext } from '../../common/hooks/use_update_executi
import { useUpgradeSecurityPackages } from '../../detection_engine/rule_management/logic/use_upgrade_security_packages';
import { useSetupDetectionEngineHealthApi } from '../../detection_engine/rule_monitoring';
import { TopValuesPopover } from '../components/top_values_popover/top_values_popover';
import { AssistantOverlay } from '../../assistant/overlay';
interface HomePageProps {
children: React.ReactNode;
@ -63,6 +64,7 @@ const HomePageComponent: React.FC<HomePageProps> = ({ children, setHeaderActionM
</DragDropContextWrapper>
<HelpMenu />
<TopValuesPopover />
<AssistantOverlay />
</>
</TourContextProvider>
</ConsoleManager>

View file

@ -7,10 +7,6 @@
import { i18n } from '@kbn/i18n';
export const ASSISTANT_TITLE = i18n.translate('xpack.securitySolution.assistant.title', {
defaultMessage: 'Elastic AI Assistant',
});
export const OVERVIEW = i18n.translate('xpack.securitySolution.navigation.overview', {
defaultMessage: 'Overview',
});

View file

@ -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; 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 { AssistantHeaderLink } from './header_link';
const mockShowAssistantOverlay = jest.fn();
const mockAssistantAvailability = jest.fn(() => ({
hasAssistantPrivilege: true,
}));
jest.mock('@kbn/elastic-assistant/impl/assistant_context', () => ({
useAssistantContext: () => ({
assistantAvailability: mockAssistantAvailability(),
showAssistantOverlay: mockShowAssistantOverlay,
}),
}));
describe('AssistantHeaderLink', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the header link text', () => {
const { queryByText, queryByTestId } = render(<AssistantHeaderLink />);
expect(queryByTestId('assistantHeaderLink')).toBeInTheDocument();
expect(queryByText('AI Assistant')).toBeInTheDocument();
});
it('should not render the header link if not authorized', () => {
mockAssistantAvailability.mockReturnValueOnce({ hasAssistantPrivilege: false });
const { queryByText, queryByTestId } = render(<AssistantHeaderLink />);
expect(queryByTestId('assistantHeaderLink')).not.toBeInTheDocument();
expect(queryByText('AI Assistant')).not.toBeInTheDocument();
});
it('should call the assistant overlay to show on click', () => {
const { queryByTestId } = render(<AssistantHeaderLink />);
queryByTestId('assistantHeaderLink')?.click();
expect(mockShowAssistantOverlay).toHaveBeenCalledTimes(1);
expect(mockShowAssistantOverlay).toHaveBeenCalledWith({ showOverlay: true });
});
});

View file

@ -14,43 +14,42 @@ import { AssistantAvatar } from '@kbn/elastic-assistant';
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
const TOOLTIP_CONTENT = i18n.translate(
'xpack.securitySolution.globalHeader.assistantHeaderLinkShortcutTooltip',
{
values: { keyboardShortcut: isMac ? '⌘ ;' : 'Ctrl ;' },
defaultMessage: 'Keyboard shortcut {keyboardShortcut}',
}
);
const LINK_LABEL = i18n.translate('xpack.securitySolution.globalHeader.assistantHeaderLink', {
defaultMessage: 'AI Assistant',
});
/**
* Elastic AI Assistant header link
*/
export const AssistantHeaderLink = React.memo(() => {
const { showAssistantOverlay } = useAssistantContext();
const keyboardShortcut = isMac ? '⌘ ;' : 'Ctrl ;';
const tooltipContent = i18n.translate(
'xpack.securitySolution.globalHeader.assistantHeaderLinkShortcutTooltip',
{
values: { keyboardShortcut },
defaultMessage: 'Keyboard shortcut {keyboardShortcut}',
}
);
export const AssistantHeaderLink = () => {
const { showAssistantOverlay, assistantAvailability } = useAssistantContext();
const showOverlay = useCallback(
() => showAssistantOverlay({ showOverlay: true }),
[showAssistantOverlay]
);
if (!assistantAvailability.hasAssistantPrivilege) {
return null;
}
return (
<EuiToolTip content={tooltipContent}>
<EuiToolTip content={TOOLTIP_CONTENT}>
<EuiHeaderLink data-test-subj="assistantHeaderLink" color="primary" onClick={showOverlay}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<AssistantAvatar size="xs" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{i18n.translate('xpack.securitySolution.globalHeader.assistantHeaderLink', {
defaultMessage: 'AI Assistant',
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>{LINK_LABEL}</EuiFlexItem>
</EuiFlexGroup>
</EuiHeaderLink>
</EuiToolTip>
);
});
AssistantHeaderLink.displayName = 'AssistantHeaderLink';
};

View file

@ -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; 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 { AssistantOverlay } from './overlay';
const mockAssistantAvailability = jest.fn(() => ({
hasAssistantPrivilege: true,
}));
jest.mock('@kbn/elastic-assistant', () => ({
AssistantOverlay: () => <div data-test-subj="assistantOverlay" />,
useAssistantContext: () => ({
assistantAvailability: mockAssistantAvailability(),
}),
}));
describe('AssistantOverlay', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the header link text', () => {
const { queryByTestId } = render(<AssistantOverlay />);
expect(queryByTestId('assistantOverlay')).toBeInTheDocument();
});
it('should not render the header link if not authorized', () => {
mockAssistantAvailability.mockReturnValueOnce({ hasAssistantPrivilege: false });
const { queryByTestId } = render(<AssistantOverlay />);
expect(queryByTestId('assistantOverlay')).not.toBeInTheDocument();
});
});

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
AssistantOverlay as ElasticAssistantOverlay,
useAssistantContext,
} from '@kbn/elastic-assistant';
export const AssistantOverlay: React.FC = () => {
const { assistantAvailability } = useAssistantContext();
if (!assistantAvailability.hasAssistantPrivilege) {
return null;
}
return <ElasticAssistantOverlay />;
};

View file

@ -0,0 +1,77 @@
/*
* 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, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { AssistantProvider as ElasticAssistantProvider } from '@kbn/elastic-assistant';
import { useKibana } from '../common/lib/kibana';
import { useAssistantTelemetry } from './use_assistant_telemetry';
import { getComments } from './get_comments';
import { augmentMessageCodeBlocks, LOCAL_STORAGE_KEY } from './helpers';
import { useConversationStore } from './use_conversation_store';
import { DEFAULT_ALLOW, DEFAULT_ALLOW_REPLACEMENT } from './content/anonymization';
import { PROMPT_CONTEXTS } from './content/prompt_contexts';
import { BASE_SECURITY_QUICK_PROMPTS } from './content/quick_prompts';
import { BASE_SECURITY_SYSTEM_PROMPTS } from './content/prompts/system';
import { useAnonymizationStore } from './use_anonymization_store';
import { useAssistantAvailability } from './use_assistant_availability';
import { APP_ID } from '../../common/constants';
const ASSISTANT_TITLE = i18n.translate('xpack.securitySolution.assistant.title', {
defaultMessage: 'Elastic AI Assistant',
});
/**
* This component configures the Elastic AI Assistant context provider for the Security Solution app.
*/
export const AssistantProvider: React.FC = ({ children }) => {
const {
http,
triggersActionsUi: { actionTypeRegistry },
docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION },
} = useKibana().services;
const { conversations, setConversations } = useConversationStore();
const getInitialConversation = useCallback(() => {
return conversations;
}, [conversations]);
const assistantAvailability = useAssistantAvailability();
const assistantTelemetry = useAssistantTelemetry();
const { defaultAllow, defaultAllowReplacement, setDefaultAllow, setDefaultAllowReplacement } =
useAnonymizationStore();
const nameSpace = `${APP_ID}.${LOCAL_STORAGE_KEY}`;
return (
<ElasticAssistantProvider
actionTypeRegistry={actionTypeRegistry}
augmentMessageCodeBlocks={augmentMessageCodeBlocks}
assistantAvailability={assistantAvailability}
assistantLangChain={false}
assistantTelemetry={assistantTelemetry}
defaultAllow={defaultAllow}
defaultAllowReplacement={defaultAllowReplacement}
docLinks={{ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }}
baseAllow={DEFAULT_ALLOW}
baseAllowReplacement={DEFAULT_ALLOW_REPLACEMENT}
basePromptContexts={Object.values(PROMPT_CONTEXTS)}
baseQuickPrompts={BASE_SECURITY_QUICK_PROMPTS}
baseSystemPrompts={BASE_SECURITY_SYSTEM_PROMPTS}
getInitialConversations={getInitialConversation}
getComments={getComments}
http={http}
nameSpace={nameSpace}
setConversations={setConversations}
setDefaultAllow={setDefaultAllow}
setDefaultAllowReplacement={setDefaultAllowReplacement}
title={ASSISTANT_TITLE}
>
{children}
</ElasticAssistantProvider>
);
};

View file

@ -5,13 +5,11 @@
* 2.0.
*/
import { AssistantOverlay } from '@kbn/elastic-assistant';
import classNames from 'classnames';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import type { CommonProps } from '@elastic/eui';
import { useAssistantAvailability } from '../../../assistant/use_assistant_availability';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { AppGlobalStyle } from '../page';
@ -42,7 +40,6 @@ interface SecuritySolutionPageWrapperProps {
const SecuritySolutionPageWrapperComponent: React.FC<
SecuritySolutionPageWrapperProps & CommonProps
> = ({ children, className, style, noPadding, noTimeline, ...otherProps }) => {
const { isAssistantEnabled, hasAssistantPrivilege } = useAssistantAvailability();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
useEffect(() => {
setGlobalFullScreen(false); // exit full screen mode on page load
@ -59,7 +56,6 @@ const SecuritySolutionPageWrapperComponent: React.FC<
<Wrapper className={classes} style={style} {...otherProps}>
{children}
<AppGlobalStyle />
{hasAssistantPrivilege && <AssistantOverlay isAssistantEnabled={isAssistantEnabled} />}
</Wrapper>
);
};

View file

@ -100,33 +100,19 @@ interface BasicTimelineTab {
}
const AssistantTab: React.FC<{
isAssistantEnabled: boolean;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
timelineId: TimelineId;
shouldRefocusPrompt: boolean;
setConversationId: Dispatch<SetStateAction<string>>;
}> = memo(
({
isAssistantEnabled,
renderCellValue,
rowRenderers,
timelineId,
shouldRefocusPrompt,
setConversationId,
}) => (
<Suspense fallback={<EuiSkeletonText lines={10} />}>
<AssistantTabContainer>
<Assistant
isAssistantEnabled={isAssistantEnabled}
conversationId={TIMELINE_CONVERSATION_TITLE}
setConversationId={setConversationId}
shouldRefocusPrompt={shouldRefocusPrompt}
/>
</AssistantTabContainer>
</Suspense>
)
);
}> = memo(({ shouldRefocusPrompt, setConversationId }) => (
<Suspense fallback={<EuiSkeletonText lines={10} />}>
<AssistantTabContainer>
<Assistant
conversationId={TIMELINE_CONVERSATION_TITLE}
setConversationId={setConversationId}
shouldRefocusPrompt={shouldRefocusPrompt}
/>
</AssistantTabContainer>
</Suspense>
));
AssistantTab.displayName = 'AssistantTab';
@ -147,7 +133,7 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
showTimeline,
}) => {
const isDiscoverInTimelineEnabled = useIsExperimentalFeatureEnabled('discoverInTimeline');
const { hasAssistantPrivilege, isAssistantEnabled } = useAssistantAvailability();
const { hasAssistantPrivilege } = useAssistantAvailability();
const getTab = useCallback(
(tab: TimelineTabs) => {
switch (tab) {
@ -235,10 +221,6 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
{(activeTimelineTab === TimelineTabs.securityAssistant ||
hasTimelineConversationStarted) && (
<AssistantTab
isAssistantEnabled={isAssistantEnabled}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
timelineId={timelineId}
setConversationId={setConversationId}
shouldRefocusPrompt={
showTimeline && activeTimelineTab === TimelineTabs.securityAssistant