[AI4SOC] AI settings page (#217373)

This commit is contained in:
Steph Milovic 2025-04-21 10:31:55 -06:00 committed by GitHub
parent 766cd47176
commit 361d38acfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 840 additions and 61 deletions

View file

@ -11,7 +11,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { HttpSetup } from '@kbn/core-http-browser';
import { FormattedMessage } from '@kbn/i18n-react';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
import { noop } from 'lodash/fp';
import { PromptResponse } from '@kbn/elastic-assistant-common';
import { Conversation } from '../../../..';
import * as i18n from './translations';
@ -214,7 +213,6 @@ export const ConversationSettingsEditor: React.FC<ConversationSettingsEditorProp
isSettingsModalVisible={true}
onSystemPromptSelectionChange={handleOnSystemPromptSelectionChange}
selectedPrompt={selectedSystemPrompt}
setIsSettingsModalVisible={noop} // noop, already in settings
/>
</EuiFormRow>

View file

@ -38,7 +38,7 @@ export interface Props {
isOpen?: boolean;
isSettingsModalVisible: boolean;
selectedPrompt: PromptResponse | undefined;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
setIsSettingsModalVisible?: React.Dispatch<React.SetStateAction<boolean>>;
onSystemPromptSelectionChange: (promptId: string | undefined) => void;
}
@ -94,7 +94,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
const onChange = useCallback(
async (selectedSystemPromptId: string) => {
if (selectedSystemPromptId === ADD_NEW_SYSTEM_PROMPT) {
setIsSettingsModalVisible(true);
setIsSettingsModalVisible?.(true);
setSelectedSettingsTab(SYSTEM_PROMPTS_TAB);
return;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { EuiAvatar, EuiPageTemplate, EuiTitle, useEuiShadow, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
@ -47,7 +47,17 @@ export const AssistantSettingsManagement: React.FC<Props> = React.memo(
const {
assistantFeatures: { assistantModelEvaluation: modelEvaluatorEnabled },
http,
selectedSettingsTab: contextSettingsTab,
setSelectedSettingsTab,
} = useAssistantContext();
useEffect(() => {
if (contextSettingsTab) {
// contextSettingsTab can be selected from Conversations > System Prompts > Add System Prompt
onTabChange?.(contextSettingsTab);
}
}, [onTabChange, contextSettingsTab, setSelectedSettingsTab]);
const { data: connectors } = useLoadConnectors({
http,
});

View file

@ -0,0 +1,139 @@
/*
* 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 { welcomeConvo } from '../../mock/conversation';
import { useAssistantContext } from '../../assistant_context';
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SearchAILakeConfigurationsSettingsManagement } from './search_ai_lake_configurations_settings_management';
import {
CONNECTORS_TAB,
ANONYMIZATION_TAB,
CONVERSATIONS_TAB,
KNOWLEDGE_BASE_TAB,
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
} from './const';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
const mockContext = {
basePromptContexts: MOCK_QUICK_PROMPTS,
http: {
get: jest.fn(),
},
assistantFeatures: { assistantModelEvaluation: true },
assistantAvailability: {
isAssistantEnabled: true,
},
};
const mockDataViews = {
getIndices: jest.fn(),
} as unknown as DataViewsContract;
const onTabChange = jest.fn();
const testProps = {
selectedConversation: welcomeConvo,
dataViews: mockDataViews,
onTabChange,
currentTab: CONNECTORS_TAB,
};
jest.mock('../../assistant_context');
jest.mock('../../connectorland/connector_settings_management', () => ({
ConnectorsSettingsManagement: () => <span data-test-subj="connectors-tab" />,
}));
jest.mock('../conversations/conversation_settings_management', () => ({
ConversationSettingsManagement: () => <span data-test-subj="conversations-tab" />,
}));
jest.mock('../quick_prompts/quick_prompt_settings_management', () => ({
QuickPromptSettingsManagement: () => <span data-test-subj="quick_prompts-tab" />,
}));
jest.mock('../prompt_editor/system_prompt/system_prompt_settings_management', () => ({
SystemPromptSettingsManagement: () => <span data-test-subj="system_prompts-tab" />,
}));
jest.mock('../../knowledge_base/knowledge_base_settings_management', () => ({
KnowledgeBaseSettingsManagement: () => <span data-test-subj="knowledge_base-tab" />,
}));
jest.mock('../../data_anonymization/settings/anonymization_settings_management', () => ({
AnonymizationSettingsManagement: () => <span data-test-subj="anonymization-tab" />,
}));
jest.mock('.', () => {
return {
EvaluationSettings: () => <span data-test-subj="evaluation-tab" />,
};
});
jest.mock('../../connectorland/use_load_connectors', () => ({
useLoadConnectors: jest.fn().mockReturnValue({ data: [] }),
}));
const queryClient = new QueryClient();
const wrapper = (props: { children: React.ReactNode }) => (
<I18nProvider>
<QueryClientProvider client={queryClient}>{props.children}</QueryClientProvider>
</I18nProvider>
);
describe('SearchAILakeConfigurationsSettingsManagement', () => {
beforeEach(() => {
jest.clearAllMocks();
(useAssistantContext as jest.Mock).mockImplementation(() => mockContext);
});
it('Bottom bar is hidden when no pending changes', async () => {
const { queryByTestId } = render(
<SearchAILakeConfigurationsSettingsManagement {...testProps} />,
{
wrapper,
}
);
expect(queryByTestId(`bottom-bar`)).not.toBeInTheDocument();
});
describe.each([
CONNECTORS_TAB,
ANONYMIZATION_TAB,
CONVERSATIONS_TAB,
KNOWLEDGE_BASE_TAB,
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
])('%s', (tab) => {
it('Opens the tab on button click', () => {
const { getByTestId } = render(
<SearchAILakeConfigurationsSettingsManagement {...testProps} currentTab={tab} />,
{
wrapper,
}
);
fireEvent.click(getByTestId(`settingsPageTab-${tab}`));
expect(onTabChange).toHaveBeenCalledWith(tab);
});
it('renders with the correct tab open', () => {
const { getByTestId } = render(
<SearchAILakeConfigurationsSettingsManagement {...testProps} currentTab={tab} />,
{
wrapper,
}
);
expect(getByTestId(`tab-${tab}`)).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,162 @@
/*
* 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, useEffect, useMemo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiListGroup,
EuiListGroupItem,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { AIForSOCConnectorSettingsManagement } from '../../connectorland/ai_for_soc_connector_settings_management';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
import { getDefaultConnector } from '../helpers';
import { ConversationSettingsManagement } from '../conversations/conversation_settings_management';
import { QuickPromptSettingsManagement } from '../quick_prompts/quick_prompt_settings_management';
import { SystemPromptSettingsManagement } from '../prompt_editor/system_prompt/system_prompt_settings_management';
import { AnonymizationSettingsManagement } from '../../data_anonymization/settings/anonymization_settings_management';
import {
ANONYMIZATION_TAB,
CONNECTORS_TAB,
CONVERSATIONS_TAB,
KNOWLEDGE_BASE_TAB,
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
} from './const';
import { KnowledgeBaseSettingsManagement } from '../../knowledge_base/knowledge_base_settings_management';
import { ManagementSettingsTabs } from './types';
interface Props {
dataViews: DataViewsContract;
onTabChange?: (tabId: string) => void;
currentTab: ManagementSettingsTabs;
}
/**
* Modal for overall Assistant Settings, including conversation settings, quick prompts, system prompts,
* anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag.
*/
export const SearchAILakeConfigurationsSettingsManagement: React.FC<Props> = React.memo(
({ dataViews, onTabChange, currentTab }) => {
const { http, selectedSettingsTab, setSelectedSettingsTab } = useAssistantContext();
useEffect(() => {
if (selectedSettingsTab) {
// selectedSettingsTab can be selected from Conversations > System Prompts > Add System Prompt
onTabChange?.(selectedSettingsTab);
}
}, [onTabChange, selectedSettingsTab, setSelectedSettingsTab]);
const { data: connectors } = useLoadConnectors({
http,
});
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
const { euiTheme } = useEuiTheme();
const tabsConfig = useMemo(
() => [
{
id: CONVERSATIONS_TAB,
label: i18n.CONVERSATIONS_MENU_ITEM,
},
{
id: CONNECTORS_TAB,
label: i18n.CONNECTORS_MENU_ITEM,
},
{
id: SYSTEM_PROMPTS_TAB,
label: i18n.SYSTEM_PROMPTS_MENU_ITEM,
},
{
id: QUICK_PROMPTS_TAB,
label: i18n.QUICK_PROMPTS_MENU_ITEM,
},
{
id: ANONYMIZATION_TAB,
label: i18n.ANONYMIZATION_MENU_ITEM,
},
{
id: KNOWLEDGE_BASE_TAB,
label: i18n.KNOWLEDGE_BASE_MENU_ITEM,
},
],
[]
);
const tabs = useMemo(() => {
return tabsConfig.map((t) => ({
...t,
onClick: () => {
onTabChange?.(t.id);
},
isSelected: t.id === currentTab,
}));
}, [onTabChange, currentTab, tabsConfig]);
const renderTabBody = useCallback(() => {
switch (currentTab) {
case CONNECTORS_TAB:
return <AIForSOCConnectorSettingsManagement />;
case SYSTEM_PROMPTS_TAB:
return (
<SystemPromptSettingsManagement
connectors={connectors}
defaultConnector={defaultConnector}
/>
);
case QUICK_PROMPTS_TAB:
return <QuickPromptSettingsManagement />;
case ANONYMIZATION_TAB:
return <AnonymizationSettingsManagement />;
case KNOWLEDGE_BASE_TAB:
return <KnowledgeBaseSettingsManagement dataViews={dataViews} />;
case CONVERSATIONS_TAB:
default:
return (
<ConversationSettingsManagement
connectors={connectors}
defaultConnector={defaultConnector}
/>
);
}
}, [connectors, currentTab, dataViews, defaultConnector]);
return (
<EuiFlexGroup
data-test-subj="SearchAILakeConfigurationsSettingsManagement"
css={css`
margin-top: ${euiTheme.size.l};
`}
>
<EuiFlexItem grow={false} css={{ width: '200px' }}>
<EuiListGroup flush>
{tabs.map(({ id, label, onClick, isSelected }) => (
<EuiListGroupItem
key={id}
label={label}
onClick={onClick}
data-test-subj={`settingsPageTab-${id}`}
isActive={isSelected}
size="s"
/>
))}
</EuiListGroup>
</EuiFlexItem>
<EuiFlexItem data-test-subj={`tab-${currentTab}`}>{renderTabBody()}</EuiFlexItem>
</EuiFlexGroup>
);
}
);
SearchAILakeConfigurationsSettingsManagement.displayName =
'SearchAILakeConfigurationsSettingsManagement';

View file

@ -8,9 +8,15 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../mock/test_providers/test_providers';
import {
mockAssistantAvailability,
TestProviders,
} from '../../../mock/test_providers/test_providers';
import { SettingsContextMenu } from './settings_context_menu';
import { AI_ASSISTANT_MENU } from './translations';
import { alertConvo, conversationWithContentReferences } from '../../../mock/conversation';
import { SecurityPageName } from '@kbn/deeplinks-security';
import { KNOWLEDGE_BASE_TAB } from '../const';
describe('SettingsContextMenu', () => {
it('renders an accessible menu button icon', () => {
@ -22,4 +28,160 @@ describe('SettingsContextMenu', () => {
expect(screen.getByRole('button', { name: AI_ASSISTANT_MENU })).toBeInTheDocument();
});
it('renders all menu items', () => {
render(
<TestProviders>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
expect(screen.getByTestId('alerts-to-analyze')).toBeInTheDocument();
expect(screen.getByTestId('anonymize-values')).toBeInTheDocument();
expect(screen.getByTestId('show-citations')).toBeInTheDocument();
expect(screen.getByTestId('clear-chat')).toBeInTheDocument();
});
it('triggers the reset conversation modal when clicking RESET_CONVERSATION', () => {
render(
<TestProviders>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
screen.getByTestId('clear-chat').click();
expect(screen.getByTestId('reset-conversation-modal')).toBeInTheDocument();
});
it('disables the anonymize values switch when no anonymized fields are present', () => {
render(
<TestProviders>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
const anonymizeSwitch = screen.getByTestId('anonymize-switch');
expect(anonymizeSwitch).toBeDisabled();
});
it('enables the anonymize values switch when no anonymized fields are present', () => {
render(
<TestProviders>
<SettingsContextMenu selectedConversation={alertConvo} />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
const anonymizeSwitch = screen.getByTestId('anonymize-switch');
expect(anonymizeSwitch).not.toBeDisabled();
});
it('disables the show citations switch when no citations are present', () => {
render(
<TestProviders>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
const citationsSwitch = screen.getByTestId('citations-switch');
expect(citationsSwitch).toBeDisabled();
});
it('enables the show citations switch when no citations are present', () => {
render(
<TestProviders>
<SettingsContextMenu selectedConversation={conversationWithContentReferences} />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
const citationsSwitch = screen.getByTestId('citations-switch');
expect(citationsSwitch).not.toBeDisabled();
});
it('Navigates to AI settings for non-AI4SOC', () => {
const mockNavigateToApp = jest.fn();
render(
<TestProviders providerContext={{ navigateToApp: mockNavigateToApp }}>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
screen.getByTestId('ai-assistant-settings').click();
expect(mockNavigateToApp).toHaveBeenCalledWith('management', {
path: 'kibana/securityAiAssistantManagement',
});
});
it('Navigates to AI settings for AI4SOC', () => {
const mockNavigateToApp = jest.fn();
render(
<TestProviders
assistantAvailability={{
...mockAssistantAvailability,
hasSearchAILakeConfigurations: true,
}}
providerContext={{ navigateToApp: mockNavigateToApp }}
>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
screen.getByTestId('ai-assistant-settings').click();
expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolutionUI', {
deepLinkId: SecurityPageName.configurationsAiSettings,
});
});
it('Navigates to Knowledge Base for non-AI4SOC', () => {
const mockNavigateToApp = jest.fn();
render(
<TestProviders providerContext={{ navigateToApp: mockNavigateToApp }}>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
screen.getByTestId('knowledge-base').click();
expect(mockNavigateToApp).toHaveBeenCalledWith('management', {
path: `kibana/securityAiAssistantManagement?tab=${KNOWLEDGE_BASE_TAB}`,
});
});
it('Navigates to Knowledge Base for AI4SOC', () => {
const mockNavigateToApp = jest.fn();
render(
<TestProviders
assistantAvailability={{
...mockAssistantAvailability,
hasSearchAILakeConfigurations: true,
}}
providerContext={{ navigateToApp: mockNavigateToApp }}
>
<SettingsContextMenu />
</TestProviders>
);
screen.getByTestId('chat-context-menu').click();
screen.getByTestId('knowledge-base').click();
expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolutionUI', {
deepLinkId: SecurityPageName.configurationsAiSettings,
path: `?tab=${KNOWLEDGE_BASE_TAB}`,
});
});
});

View file

@ -26,6 +26,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import { SecurityPageName } from '@kbn/deeplinks-security';
import { KnowledgeBaseTour } from '../../../tour/knowledge_base';
import { AnonymizationSettingsManagement } from '../../../data_anonymization/settings/anonymization_settings_management';
import { Conversation, useAssistantContext } from '../../../..';
@ -60,12 +61,14 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
({ isDisabled = false, onChatCleared, selectedConversation }: Params) => {
const { euiTheme } = useEuiTheme();
const {
assistantAvailability,
navigateToApp,
knowledgeBase,
setContentReferencesVisible,
contentReferencesVisible,
showAnonymizedValues,
setShowAnonymizedValues,
showAssistantOverlay,
} = useAssistantContext();
const [isPopoverOpen, setPopover] = useState(false);
@ -95,26 +98,37 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
setIsResetConversationModalVisible(true);
}, [closePopover]);
const handleNavigateToSettings = useCallback(
() =>
const handleNavigateToSettings = useCallback(() => {
if (assistantAvailability.hasSearchAILakeConfigurations) {
navigateToApp('securitySolutionUI', {
deepLinkId: SecurityPageName.configurationsAiSettings,
});
showAssistantOverlay?.({ showOverlay: false });
} else {
navigateToApp('management', {
path: 'kibana/securityAiAssistantManagement',
}),
[navigateToApp]
);
});
}
}, [assistantAvailability.hasSearchAILakeConfigurations, navigateToApp, showAssistantOverlay]);
const handleNavigateToAnonymization = useCallback(() => {
showAnonymizationModal();
closePopover();
}, [closePopover, showAnonymizationModal]);
const handleNavigateToKnowledgeBase = useCallback(
() =>
const handleNavigateToKnowledgeBase = useCallback(() => {
if (assistantAvailability.hasSearchAILakeConfigurations) {
navigateToApp('securitySolutionUI', {
deepLinkId: SecurityPageName.configurationsAiSettings,
path: `?tab=${KNOWLEDGE_BASE_TAB}`,
});
showAssistantOverlay?.({ showOverlay: false });
} else {
navigateToApp('management', {
path: `kibana/securityAiAssistantManagement?tab=${KNOWLEDGE_BASE_TAB}`,
}),
[navigateToApp]
);
});
}
}, [assistantAvailability.hasSearchAILakeConfigurations, navigateToApp, showAssistantOverlay]);
const handleShowAlertsModal = useCallback(() => {
showAlertSettingsModal();
@ -215,6 +229,7 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
<EuiToolTip
position="top"
key={'disabled-anonymize-values-tooltip'}
data-test-subj={'disabled-anonymize-values-tooltip'}
content={
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.anonymizeValues.disabled.tooltip"
@ -227,6 +242,7 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
)}
>
<EuiSwitch
data-test-subj={'anonymize-switch'}
label={i18n.ANONYMIZE_VALUES}
checked={showAnonymizedValues}
onChange={onChangeShowAnonymizedValues}
@ -267,7 +283,8 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
wrap={(children) => (
<EuiToolTip
position="top"
key={'disabled-anonymize-values-tooltip'}
key={'disabled-citations-values-tooltip'}
data-test-subj={'disabled-citations-values-tooltip'}
content={
<FormattedMessage
id="xpack.elasticAssistant.assistant.settings.showCitationsLabel.disabled.tooltip"
@ -280,6 +297,7 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
)}
>
<EuiSwitch
data-test-subj={'citations-switch'}
label={i18n.SHOW_CITATIONS}
checked={contentReferencesVisible}
onChange={onChangeContentReferencesVisible}

View file

@ -60,6 +60,8 @@ export interface AssistantTelemetry {
}
export interface AssistantAvailability {
// True when searchAiLake configurations is available
hasSearchAILakeConfigurations: boolean;
// True when user is Enterprise, or Security Complete PLI for serverless. When false, the Assistant is disabled and unavailable
isAssistantEnabled: boolean;
// When true, the Assistant is hidden and unavailable

View file

@ -0,0 +1,21 @@
/*
* 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 { EuiSpacer } from '@elastic/eui';
import { SearchConnectorSettingsManagement } from './search_connector_settings_management';
import { ConnectorsSettingsManagement } from '../connector_settings_management';
export const AIForSOCConnectorSettingsManagement = () => {
return (
<>
<ConnectorsSettingsManagement />
<EuiSpacer size="m" />
<SearchConnectorSettingsManagement />
</>
);
};

View file

@ -0,0 +1,61 @@
/*
* 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 {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback } from 'react';
import { useAssistantContext } from '../../assistant_context';
import * as i18n from './translations';
const SearchConnectorSettingsManagementComponent: React.FC = () => {
const { navigateToApp } = useAssistantContext();
const onClick = useCallback(
() =>
navigateToApp('management', {
path: 'data/content_connectors/connectors',
}),
[navigateToApp]
);
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
<EuiTitle size="xs">
<h2>{i18n.CONTENT_CONNECTORS_TITLE}</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem
css={css`
align-self: center;
`}
>
<EuiText size="m">{i18n.CONTENT_CONNECTORS_DESCRIPTION}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onClick}>{i18n.SETTINGS_MANAGE_CONNECTORS}</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};
export const SearchConnectorSettingsManagement = React.memo(
SearchConnectorSettingsManagementComponent
);
SearchConnectorSettingsManagementComponent.displayName =
'SearchConnectorSettingsManagementComponent';

View file

@ -0,0 +1,26 @@
/*
* 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.
*/
// Search Connectors
import { i18n } from '@kbn/i18n';
export const CONTENT_CONNECTORS_TITLE = i18n.translate(
'xpack.elasticAssistant.aiSettings.contentConnectors.title',
{ defaultMessage: 'Content connectors' }
);
export const CONTENT_CONNECTORS_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.aiSettings.contentConnectors.description',
{
defaultMessage: 'Use content connectors to add knowledge to the Knowledge Base.',
}
);
export const SETTINGS_MANAGE_CONNECTORS = i18n.translate(
'xpack.elasticAssistant.aiSettings.settings.manageConnectors',
{ defaultMessage: 'Manage connectors' }
);

View file

@ -10,18 +10,15 @@ import React from 'react';
import { render } from '@testing-library/react';
import { ConnectorMissingCallout } from '.';
import { AssistantAvailability } from '../../..';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { mockAssistantAvailability, TestProviders } from '../../mock/test_providers/test_providers';
describe('connectorMissingCallout', () => {
describe('when connectors and actions privileges', () => {
describe('are `READ`', () => {
const assistantAvailability: AssistantAvailability = {
...mockAssistantAvailability,
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: false,
hasConnectorsReadPrivilege: true,
hasUpdateAIAssistantAnonymization: true,
hasManageGlobalKnowledgeBase: true,
isAssistantEnabled: true,
};
it('should show connector privileges required button if no connectors exist', async () => {
@ -55,12 +52,10 @@ describe('connectorMissingCallout', () => {
describe('are `NONE`', () => {
const assistantAvailability: AssistantAvailability = {
...mockAssistantAvailability,
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: false,
hasConnectorsReadPrivilege: false,
hasUpdateAIAssistantAnonymization: true,
hasManageGlobalKnowledgeBase: false,
isAssistantEnabled: true,
};
it('should show connector privileges required button', async () => {

View file

@ -8,9 +8,9 @@
import { i18n } from '@kbn/i18n';
export const CONNECTOR_SETTINGS_MANAGEMENT_TITLE = i18n.translate(
'xpack.elasticAssistant.connectors.connectorSettingsManagement.title',
'xpack.elasticAssistant.connectors.connectorSettingsManagement.title.llm',
{
defaultMessage: 'Settings',
defaultMessage: 'LLM connectors',
}
);

View file

@ -30,6 +30,7 @@ window.scrollTo = jest.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
export const mockAssistantAvailability: AssistantAvailability = {
hasSearchAILakeConfigurations: false,
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,

View file

@ -169,6 +169,9 @@ export {
/** Your anonymization settings will apply to these alerts (label) */
YOUR_ANONYMIZATION_SETTINGS,
} from './impl/knowledge_base/translations';
export { SearchAILakeConfigurationsSettingsManagement } from './impl/assistant/settings/search_ai_lake_configurations_settings_management';
export { CONVERSATIONS_TAB } from './impl/assistant/settings/const';
export type { ManagementSettingsTabs } from './impl/assistant/settings/types';
export {
AlertSummary,

View file

@ -43,6 +43,7 @@
"@kbn/shared-ux-router",
"@kbn/inference-endpoint-ui-common",
"@kbn/datemath",
"@kbn/alerts-ui-shared"
"@kbn/alerts-ui-shared",
"@kbn/deeplinks-security"
]
}

View file

@ -16484,7 +16484,6 @@
"xpack.elasticAssistant.components.upgrade.upgradeTitle": "Gérer la licence",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.buttonTitle": "Gérer les connecteurs",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.description": "Pour utiliser Elastic AI Assistant, vous devez configurer un connecteur sur un modèle de langage externe de plus grande envergure.",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.title": "Paramètres",
"xpack.elasticAssistant.connectors.models.modelSelector.customOptionText": "Créer un nouveau modèle appelé",
"xpack.elasticAssistant.connectors.models.modelSelector.helpLabel": "Modèle utilisé pour ce connecteur",
"xpack.elasticAssistant.connectors.models.modelSelector.modelTitle": "Modèle",

View file

@ -16463,7 +16463,6 @@
"xpack.elasticAssistant.components.upgrade.upgradeTitle": "ライセンスの管理",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.buttonTitle": "コネクターを管理",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.description": "Elastic AI Assistantを使用するには、コネクターを既存の大規模言語モデルに設定する必要があります。",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.title": "設定",
"xpack.elasticAssistant.connectors.models.modelSelector.customOptionText": "新しい名前付きモデルを作成",
"xpack.elasticAssistant.connectors.models.modelSelector.helpLabel": "このコネクターで使用するモデル",
"xpack.elasticAssistant.connectors.models.modelSelector.modelTitle": "モデル",

View file

@ -16500,7 +16500,6 @@
"xpack.elasticAssistant.components.upgrade.upgradeTitle": "管理许可证",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.buttonTitle": "管理连接器",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.description": "要使用 Elastic AI Assistant必须设置连接外部大语言模型的连接器。",
"xpack.elasticAssistant.connectors.connectorSettingsManagement.title": "设置",
"xpack.elasticAssistant.connectors.models.modelSelector.customOptionText": "创建新的已命名模型",
"xpack.elasticAssistant.connectors.models.modelSelector.helpLabel": "要用于此连接器的模型",
"xpack.elasticAssistant.connectors.models.modelSelector.modelTitle": "模型",

View file

@ -48,6 +48,7 @@ const TestExternalProvidersComponent: React.FC<TestExternalProvidersProps> = ({
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
const mockNavigateToApp = jest.fn();
const mockAssistantAvailability: AssistantAvailability = {
hasSearchAILakeConfigurations: false,
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,

View file

@ -11,7 +11,7 @@ import { GlobalHeader } from '.';
import {
ADD_DATA_PATH,
ADD_THREAT_INTELLIGENCE_DATA_PATH,
SecurityPageName,
SECURITY_FEATURE_ID,
THREAT_INTELLIGENCE_PATH,
} from '../../../../common/constants';
import {
@ -25,6 +25,8 @@ import { sourcererPaths } from '../../../sourcerer/containers/sourcerer_paths';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { DATA_VIEW_PICKER_TEST_ID } from '../../../data_view_manager/components/data_view_picker/constants';
import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__';
import { useKibana } from '../../../common/lib/kibana';
jest.mock('../../../common/hooks/use_experimental_features');
@ -63,11 +65,23 @@ describe('global header', () => {
},
};
const store = createMockStore(state);
beforeEach(() => {
jest.clearAllMocks();
(useKibana as jest.Mock).mockReturnValue({
...mockUseKibana(),
services: {
...mockUseKibana().services,
application: {
capabilities: {
[SECURITY_FEATURE_ID]: {
configurations: false,
},
},
},
},
});
});
it('has add data link', () => {
(useLocation as jest.Mock).mockReturnValue([
{ pageName: SecurityPageName.overview, detailName: undefined },
]);
const { getByText } = render(
<TestProviders store={store}>
<GlobalHeader />
@ -77,9 +91,6 @@ describe('global header', () => {
});
it('points to the default Add data URL', () => {
(useLocation as jest.Mock).mockReturnValue([
{ pageName: SecurityPageName.overview, detailName: undefined },
]);
const { queryByTestId } = render(
<TestProviders store={store}>
<GlobalHeader />
@ -89,6 +100,28 @@ describe('global header', () => {
expect(link?.getAttribute('href')).toBe(ADD_DATA_PATH);
});
it('does not show the default Add data URL when hasSearchAILakeConfigurations', () => {
(useKibana as jest.Mock).mockReturnValue({
...mockUseKibana(),
services: {
...mockUseKibana().services,
application: {
capabilities: {
[SECURITY_FEATURE_ID]: {
configurations: true,
},
},
},
},
});
const { queryByTestId } = render(
<TestProviders store={store}>
<GlobalHeader />
</TestProviders>
);
expect(queryByTestId('add-data')).not.toBeInTheDocument();
});
it('points to the threat_intel Add data URL for threat_intelligence url', () => {
(useLocation as jest.Mock).mockReturnValue({ pathname: THREAT_INTELLIGENCE_PATH });
const { queryByTestId } = render(
@ -149,10 +182,6 @@ describe('global header', () => {
});
it('shows AI Assistant header link', () => {
(useLocation as jest.Mock).mockReturnValue([
{ pageName: SecurityPageName.overview, detailName: undefined },
]);
const { findByTestId } = render(
<TestProviders store={store}>
<GlobalHeader />

View file

@ -16,6 +16,7 @@ import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { SECURITY_FEATURE_ID } from '../../../../common';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
import { useKibana } from '../../../common/lib/kibana';
@ -42,7 +43,13 @@ const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.butt
*/
export const GlobalHeader = React.memo(() => {
const portalNode = useMemo(() => createHtmlPortalNode(), []);
const { theme, setHeaderActionMenu, i18n: kibanaServiceI18n } = useKibana().services;
const {
theme,
setHeaderActionMenu,
i18n: kibanaServiceI18n,
application: { capabilities },
} = useKibana().services;
const hasSearchAILakeConfigurations = capabilities[SECURITY_FEATURE_ID]?.configurations === true;
const { pathname } = useLocation();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
@ -96,15 +103,17 @@ export const GlobalHeader = React.memo(() => {
<EuiHeaderSectionItem>
<EuiHeaderLinks>
<EuiHeaderLink
color="primary"
data-test-subj="add-data"
href={href}
iconType="indexOpen"
onClick={onClick}
>
{BUTTON_ADD_DATA}
</EuiHeaderLink>
{!hasSearchAILakeConfigurations && (
<EuiHeaderLink
color="primary"
data-test-subj="add-data"
href={href}
iconType="indexOpen"
onClick={onClick}
>
{BUTTON_ADD_DATA}
</EuiHeaderLink>
)}
{showSourcerer && !showTimeline && dataViewPicker}
</EuiHeaderLinks>
</EuiHeaderSectionItem>

View file

@ -7,9 +7,11 @@
import { useLicense } from '../../common/hooks/use_license';
import { useKibana } from '../../common/lib/kibana';
import { ASSISTANT_FEATURE_ID } from '../../../common/constants';
import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../../common/constants';
export interface UseAssistantAvailability {
// True when searchAiLake configurations is available
hasSearchAILakeConfigurations: boolean;
// True when user is Enterprise. When false, the Assistant is disabled and unavailable
isAssistantEnabled: boolean;
// When true, the Assistant is hidden and unavailable
@ -32,6 +34,7 @@ export const useAssistantAvailability = (): UseAssistantAvailability => {
capabilities[ASSISTANT_FEATURE_ID]?.updateAIAssistantAnonymization === true;
const hasManageGlobalKnowledgeBase =
capabilities[ASSISTANT_FEATURE_ID]?.manageGlobalKnowledgeBaseAIAssistant === true;
const hasSearchAILakeConfigurations = capabilities[SECURITY_FEATURE_ID]?.configurations === true;
// Connectors & Actions capabilities as defined in x-pack/plugins/actions/server/feature.ts
// `READ` ui capabilities defined as: { ui: ['show', 'execute'] }
@ -44,6 +47,7 @@ export const useAssistantAvailability = (): UseAssistantAvailability => {
capabilities.actions?.save === true;
return {
hasSearchAILakeConfigurations,
hasAssistantPrivilege,
hasConnectorsAllPrivilege,
hasConnectorsReadPrivilege,

View file

@ -31,6 +31,7 @@ export const MockAssistantProviderComponent: React.FC<Props> = ({
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
const mockNavigateToApp = jest.fn();
const defaultAssistantAvailability: AssistantAvailability = {
hasSearchAILakeConfigurations: false,
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,

View file

@ -9,8 +9,8 @@ import React from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';
import { Redirect } from 'react-router-dom';
import { AISettings } from '../tabs/ai_settings';
import { BasicRules } from '../tabs/basic_rules';
import { AiSettings } from '../tabs/ai_settings';
import { CONFIGURATIONS_PATH } from '../../../common/constants';
import { ConfigurationTabs } from '../constants';
import { LazyConfigurationsIntegrationsHome } from '../tabs/integrations';
@ -24,7 +24,7 @@ export const ConfigurationsRouter = React.memo(() => {
/>
<Route
path={`${CONFIGURATIONS_PATH}/:tab(${ConfigurationTabs.aiSettings})`}
component={AiSettings}
component={AISettings}
/>
<Route
path={`${CONFIGURATIONS_PATH}/:tab(${ConfigurationTabs.basicRules})`}

View file

@ -0,0 +1,90 @@
/*
* 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 { fireEvent, render } from '@testing-library/react';
import { MemoryRouter } from '@kbn/shared-ux-router';
import { AISettings } from './ai_settings';
import { useKibana, useNavigation } from '../../common/lib/kibana';
import { TestProviders } from '../../common/mock';
import { CONVERSATIONS_TAB } from '@kbn/elastic-assistant';
import { SecurityPageName } from '@kbn/deeplinks-security';
const mockNavigateTo = jest.fn();
jest.mock('../../common/lib/kibana');
describe('AISettings', () => {
beforeEach(() => {
jest.clearAllMocks();
(useKibana as jest.Mock).mockReturnValue({
services: {
application: {
navigateToApp: jest.fn(),
capabilities: {
securitySolutionAssistant: { 'ai-assistant': true },
},
},
data: { dataViews: {} },
},
});
(useNavigation as jest.Mock).mockReturnValue({
navigateTo: mockNavigateTo,
});
});
it('renders the SearchAILakeConfigurationsSettingsManagement component wiht default Conversations tab when securityAIAssistantEnabled is true', () => {
const { getByTestId } = render(
<MemoryRouter>
<TestProviders>
<AISettings />
</TestProviders>
</MemoryRouter>
);
expect(getByTestId('SearchAILakeConfigurationsSettingsManagement')).toBeInTheDocument();
expect(getByTestId(`tab-${CONVERSATIONS_TAB}`)).toBeInTheDocument();
});
it('onTabChange calls navigateTo with proper tab', () => {
const { getByTestId } = render(
<MemoryRouter>
<TestProviders>
<AISettings />
</TestProviders>
</MemoryRouter>
);
fireEvent.click(getByTestId(`settingsPageTab-connectors`));
expect(mockNavigateTo).toHaveBeenCalledWith({
deepLinkId: SecurityPageName.configurationsAiSettings,
path: `?tab=connectors`,
});
});
it('navigates to the home app when securityAIAssistantEnabled is false', () => {
const mockNavigateToApp = jest.fn();
(useKibana as jest.Mock).mockReturnValue({
services: {
application: {
navigateToApp: mockNavigateToApp,
capabilities: {
securitySolutionAssistant: { 'ai-assistant': false },
},
},
data: { dataViews: {} },
},
});
render(
<MemoryRouter>
<TestProviders>
<AISettings />
</TestProviders>
</MemoryRouter>
);
expect(mockNavigateToApp).toHaveBeenCalledWith('home');
});
});

View file

@ -4,8 +4,52 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as React from 'react';
export const AiSettings = () => {
return <h1>{'AI settings'}</h1>;
import React, { useCallback, useMemo } from 'react';
import {
SearchAILakeConfigurationsSettingsManagement,
CONVERSATIONS_TAB,
type ManagementSettingsTabs,
} from '@kbn/elastic-assistant';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { SecurityPageName } from '../../../common/constants';
import { useKibana, useNavigation } from '../../common/lib/kibana';
export const AISettings: React.FC = () => {
const { navigateTo } = useNavigation();
const {
application: {
navigateToApp,
capabilities: {
securitySolutionAssistant: { 'ai-assistant': securityAIAssistantEnabled },
},
},
data: { dataViews },
} = useKibana().services;
const onTabChange = useCallback(
(tab: string) => {
navigateTo({
deepLinkId: SecurityPageName.configurationsAiSettings,
path: `?tab=${tab}`,
});
},
[navigateTo]
);
const [searchParams] = useSearchParams();
const currentTab = useMemo(
() => (searchParams.get('tab') as ManagementSettingsTabs) ?? CONVERSATIONS_TAB,
[searchParams]
);
if (!securityAIAssistantEnabled) {
navigateToApp('home');
}
return (
<SearchAILakeConfigurationsSettingsManagement
dataViews={dataViews}
onTabChange={onTabChange}
currentTab={currentTab}
/>
);
};

View file

@ -29,6 +29,7 @@ describe('useAssistant', () => {
it(`should return showAssistant true and a value for promptContextId`, () => {
jest.mocked(useAssistantAvailability).mockReturnValue({
hasSearchAILakeConfigurations: false,
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
@ -48,6 +49,7 @@ describe('useAssistant', () => {
it(`should return showAssistant false if hasAssistantPrivilege is false`, () => {
jest.mocked(useAssistantAvailability).mockReturnValue({
hasSearchAILakeConfigurations: false,
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
@ -67,6 +69,7 @@ describe('useAssistant', () => {
it('returns anonymized prompt context data', async () => {
jest.mocked(useAssistantAvailability).mockReturnValue({
hasSearchAILakeConfigurations: false,
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,

View file

@ -24,6 +24,7 @@ import type {
SecuritySolutionAppWrapperFeature,
SecuritySolutionCellRendererFeature,
} from '@kbn/discover-shared-plugin/public/services/discover_features';
import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import { ProductFeatureAssistantKey } from '@kbn/security-solution-features/src/product_features_keys';
import { getLazyCloudSecurityPosturePliAuthBlockExtension } from './cloud_security_posture/lazy_cloud_security_posture_pli_auth_block_extension';
import { getLazyEndpointAgentTamperProtectionExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_agent_tamper_protection_extension';
@ -211,14 +212,15 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
return;
}
const isAssistantAvailable =
const shouldShowAssistantManagement =
productFeatureKeys?.has(ProductFeatureAssistantKey.assistant) &&
!productFeatureKeys?.has(ProductFeatureSecurityKey.configurations) &&
license?.hasAtLeast('enterprise');
const assistantManagementApp = management?.sections.section.kibana.getApp(
'securityAiAssistantManagement'
);
if (!isAssistantAvailable) {
if (!shouldShowAssistantManagement) {
assistantManagementApp?.disable();
}
});