mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[AI4SOC] AI settings page (#217373)
This commit is contained in:
parent
766cd47176
commit
361d38acfc
29 changed files with 840 additions and 61 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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' }
|
||||
);
|
|
@ -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 () => {
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "モデル",
|
||||
|
|
|
@ -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": "模型",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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})`}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue