mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.16`: - [[Security Assistant] Fix KB output fields (#196567)](https://github.com/elastic/kibana/pull/196567) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Patryk Kopyciński","email":"contact@patrykkopycinski.com"},"sourceCommit":{"committedDate":"2024-10-21T18:32:06Z","message":"[Security Assistant] Fix KB output fields (#196567)\n\n## Summary\r\n\r\nFixes Assistant Knowledge Base output fields field logic\r\nFixes Security Assistant card not appearing on Serverless \r\nReverts Assistant Cog wheel settings button when FF\r\n`assistantKnowledgeBaseByDefault` is off\r\n\r\n\r\n","sha":"399aed9b19935651b979dc68ad88429a156dae2f","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","backport:prev-minor","Feature:Security Assistant","Team:Security Generative AI","v8.16.0","v8.17.0"],"title":"[Security Assistant] Reverts Assistant Cog wheel settings button when assistantKnowledgeBaseByDefault FF is off","number":196567,"url":"https://github.com/elastic/kibana/pull/196567","mergeCommit":{"message":"[Security Assistant] Fix KB output fields (#196567)\n\n## Summary\r\n\r\nFixes Assistant Knowledge Base output fields field logic\r\nFixes Security Assistant card not appearing on Serverless \r\nReverts Assistant Cog wheel settings button when FF\r\n`assistantKnowledgeBaseByDefault` is off\r\n\r\n\r\n","sha":"399aed9b19935651b979dc68ad88429a156dae2f"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196567","number":196567,"mergeCommit":{"message":"[Security Assistant] Fix KB output fields (#196567)\n\n## Summary\r\n\r\nFixes Assistant Knowledge Base output fields field logic\r\nFixes Security Assistant card not appearing on Serverless \r\nReverts Assistant Cog wheel settings button when FF\r\n`assistantKnowledgeBaseByDefault` is off\r\n\r\n\r\n","sha":"399aed9b19935651b979dc68ad88429a156dae2f"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
parent
52483614f5
commit
18b517a9fb
8 changed files with 406 additions and 65 deletions
|
@ -23,6 +23,7 @@ import { Conversation } from '../assistant_context/types';
|
|||
import * as all from './chat_send/use_chat_send';
|
||||
import { useConversation } from './use_conversation';
|
||||
import { AIConnector } from '../connectorland/connector_selector';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
jest.mock('../connectorland/use_load_connectors');
|
||||
jest.mock('../connectorland/connector_setup');
|
||||
|
@ -138,6 +139,112 @@ describe('Assistant', () => {
|
|||
>);
|
||||
});
|
||||
|
||||
describe('persistent storage', () => {
|
||||
it('should refetchCurrentUserConversations after settings save button click', async () => {
|
||||
const chatSendSpy = jest.spyOn(all, 'useChatSend');
|
||||
await renderAssistant();
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings'));
|
||||
|
||||
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
|
||||
data: {
|
||||
...mockData,
|
||||
welcome_id: {
|
||||
...mockData.welcome_id,
|
||||
apiConfig: { newProp: true },
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
refetch: jest.fn().mockResolvedValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
...mockData,
|
||||
welcome_id: {
|
||||
...mockData.welcome_id,
|
||||
apiConfig: { newProp: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
isFetched: true,
|
||||
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('save-button'));
|
||||
});
|
||||
|
||||
expect(chatSendSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
currentConversation: {
|
||||
apiConfig: { newProp: true },
|
||||
category: 'assistant',
|
||||
id: mockData.welcome_id.id,
|
||||
messages: [],
|
||||
title: 'Welcome',
|
||||
replacements: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should refetchCurrentUserConversations after settings save button click, but do not update convos when refetch returns bad results', async () => {
|
||||
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
|
||||
data: mockData,
|
||||
isLoading: false,
|
||||
refetch: jest.fn().mockResolvedValue({
|
||||
isLoading: false,
|
||||
data: omit(mockData, 'welcome_id'),
|
||||
}),
|
||||
isFetched: true,
|
||||
} as unknown as DefinedUseQueryResult<Record<string, Conversation>, unknown>);
|
||||
const chatSendSpy = jest.spyOn(all, 'useChatSend');
|
||||
await renderAssistant();
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings'));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('save-button'));
|
||||
});
|
||||
|
||||
expect(chatSendSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
currentConversation: {
|
||||
apiConfig: { connectorId: '123' },
|
||||
replacements: {},
|
||||
category: 'assistant',
|
||||
id: mockData.welcome_id.id,
|
||||
messages: [],
|
||||
title: 'Welcome',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete conversation when delete button is clicked', async () => {
|
||||
await renderAssistant();
|
||||
const deleteButton = screen.getAllByTestId('delete-option')[0];
|
||||
await act(async () => {
|
||||
fireEvent.click(deleteButton);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteConvo).toHaveBeenCalledWith(mockData.electric_sheep_id.id);
|
||||
});
|
||||
});
|
||||
it('should refetchCurrentUserConversations after clear chat history button click', async () => {
|
||||
await renderAssistant();
|
||||
fireEvent.click(screen.getByTestId('chat-context-menu'));
|
||||
fireEvent.click(screen.getByTestId('clear-chat'));
|
||||
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
|
||||
await waitFor(() => {
|
||||
expect(clearConversation).toHaveBeenCalled();
|
||||
expect(refetchResults).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when selected conversation changes and some connectors are loaded', () => {
|
||||
it('should persist the conversation id to local storage', async () => {
|
||||
const getConversation = jest.fn().mockResolvedValue(mockData.electric_sheep_id);
|
||||
|
|
|
@ -38,7 +38,7 @@ const mockContext = {
|
|||
basePromptContexts: MOCK_QUICK_PROMPTS,
|
||||
setSelectedSettingsTab,
|
||||
http: {},
|
||||
assistantFeatures: { assistantModelEvaluation: true },
|
||||
assistantFeatures: { assistantModelEvaluation: true, assistantKnowledgeBaseByDefault: false },
|
||||
selectedSettingsTab: 'CONVERSATIONS_TAB',
|
||||
assistantAvailability: {
|
||||
isAssistantEnabled: true,
|
||||
|
@ -136,6 +136,17 @@ describe('AssistantSettings', () => {
|
|||
QUICK_PROMPTS_TAB,
|
||||
SYSTEM_PROMPTS_TAB,
|
||||
])('%s', (tab) => {
|
||||
it('Opens the tab on button click', () => {
|
||||
(useAssistantContext as jest.Mock).mockImplementation(() => ({
|
||||
...mockContext,
|
||||
selectedSettingsTab: tab === CONVERSATIONS_TAB ? ANONYMIZATION_TAB : CONVERSATIONS_TAB,
|
||||
}));
|
||||
const { getByTestId } = render(<AssistantSettings {...testProps} />, {
|
||||
wrapper,
|
||||
});
|
||||
fireEvent.click(getByTestId(`${tab}-button`));
|
||||
expect(setSelectedSettingsTab).toHaveBeenCalledWith(tab);
|
||||
});
|
||||
it('renders with the correct tab open', () => {
|
||||
(useAssistantContext as jest.Mock).mockImplementation(() => ({
|
||||
...mockContext,
|
||||
|
|
|
@ -9,10 +9,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiIcon,
|
||||
EuiModal,
|
||||
EuiModalFooter,
|
||||
EuiKeyPadMenu,
|
||||
EuiKeyPadMenuItem,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageSidebar,
|
||||
EuiSplitPanel,
|
||||
} from '@elastic/eui';
|
||||
|
||||
|
@ -76,7 +80,16 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
conversations,
|
||||
conversationsLoaded,
|
||||
}) => {
|
||||
const { http, toasts, selectedSettingsTab, setSelectedSettingsTab } = useAssistantContext();
|
||||
const {
|
||||
assistantFeatures: {
|
||||
assistantModelEvaluation: modelEvaluatorEnabled,
|
||||
assistantKnowledgeBaseByDefault,
|
||||
},
|
||||
http,
|
||||
toasts,
|
||||
selectedSettingsTab,
|
||||
setSelectedSettingsTab,
|
||||
} = useAssistantContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSettingsTab == null) {
|
||||
|
@ -201,6 +214,115 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
return (
|
||||
<StyledEuiModal data-test-subj={TEST_IDS.SETTINGS_MODAL} onClose={onClose}>
|
||||
<EuiPage paddingSize="none">
|
||||
{!assistantKnowledgeBaseByDefault && (
|
||||
<EuiPageSidebar
|
||||
paddingSize="xs"
|
||||
css={css`
|
||||
min-inline-size: unset !important;
|
||||
max-width: 104px;
|
||||
`}
|
||||
>
|
||||
<EuiKeyPadMenu>
|
||||
<EuiKeyPadMenuItem
|
||||
id={CONVERSATIONS_TAB}
|
||||
label={i18n.CONVERSATIONS_MENU_ITEM}
|
||||
isSelected={!selectedSettingsTab || selectedSettingsTab === CONVERSATIONS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(CONVERSATIONS_TAB)}
|
||||
data-test-subj={`${CONVERSATIONS_TAB}-button`}
|
||||
>
|
||||
<>
|
||||
<EuiIcon
|
||||
type="editorComment"
|
||||
size="xl"
|
||||
css={css`
|
||||
position: relative;
|
||||
top: -10px;
|
||||
`}
|
||||
/>
|
||||
<EuiIcon
|
||||
type="editorComment"
|
||||
size="l"
|
||||
css={css`
|
||||
position: relative;
|
||||
transform: rotateY(180deg);
|
||||
top: -7px;
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={QUICK_PROMPTS_TAB}
|
||||
label={i18n.QUICK_PROMPTS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === QUICK_PROMPTS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(QUICK_PROMPTS_TAB)}
|
||||
data-test-subj={`${QUICK_PROMPTS_TAB}-button`}
|
||||
>
|
||||
<>
|
||||
<EuiIcon type="editorComment" size="xxl" />
|
||||
<EuiIcon
|
||||
type="bolt"
|
||||
size="s"
|
||||
color="warning"
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 14px;
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={SYSTEM_PROMPTS_TAB}
|
||||
label={i18n.SYSTEM_PROMPTS_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === SYSTEM_PROMPTS_TAB}
|
||||
onClick={() => setSelectedSettingsTab(SYSTEM_PROMPTS_TAB)}
|
||||
data-test-subj={`${SYSTEM_PROMPTS_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="editorComment" size="xxl" />
|
||||
<EuiIcon
|
||||
type="storage"
|
||||
size="s"
|
||||
color="success"
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
left: 14px;
|
||||
`}
|
||||
/>
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={ANONYMIZATION_TAB}
|
||||
label={i18n.ANONYMIZATION_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === ANONYMIZATION_TAB}
|
||||
onClick={() => setSelectedSettingsTab(ANONYMIZATION_TAB)}
|
||||
data-test-subj={`${ANONYMIZATION_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="eyeClosed" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
<EuiKeyPadMenuItem
|
||||
id={KNOWLEDGE_BASE_TAB}
|
||||
label={i18n.KNOWLEDGE_BASE_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === KNOWLEDGE_BASE_TAB}
|
||||
onClick={() => setSelectedSettingsTab(KNOWLEDGE_BASE_TAB)}
|
||||
data-test-subj={`${KNOWLEDGE_BASE_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="notebookApp" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
{modelEvaluatorEnabled && (
|
||||
<EuiKeyPadMenuItem
|
||||
id={EVALUATION_TAB}
|
||||
label={i18n.EVALUATION_MENU_ITEM}
|
||||
isSelected={selectedSettingsTab === EVALUATION_TAB}
|
||||
onClick={() => setSelectedSettingsTab(EVALUATION_TAB)}
|
||||
data-test-subj={`${EVALUATION_TAB}-button`}
|
||||
>
|
||||
<EuiIcon type="crossClusterReplicationApp" size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
)}
|
||||
</EuiKeyPadMenu>
|
||||
</EuiPageSidebar>
|
||||
)}
|
||||
|
||||
<EuiPageBody paddingSize="none" panelled={true}>
|
||||
<EuiSplitPanel.Outer grow={true}>
|
||||
<EuiSplitPanel.Inner
|
||||
|
|
|
@ -11,6 +11,7 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/c
|
|||
|
||||
import { AssistantSettingsButton } from './assistant_settings_button';
|
||||
import { welcomeConvo } from '../../mock/conversation';
|
||||
import { CONVERSATIONS_TAB } from './const';
|
||||
|
||||
const setIsSettingsModalVisible = jest.fn();
|
||||
const onConversationSelected = jest.fn();
|
||||
|
@ -31,6 +32,7 @@ const testProps = {
|
|||
const setSelectedSettingsTab = jest.fn();
|
||||
const mockUseAssistantContext = {
|
||||
setSelectedSettingsTab,
|
||||
assistantFeatures: {},
|
||||
};
|
||||
jest.mock('../../assistant_context', () => {
|
||||
const original = jest.requireActual('../../assistant_context');
|
||||
|
@ -57,6 +59,13 @@ describe('AssistantSettingsButton', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Clicking the settings gear opens the conversations tab', () => {
|
||||
const { getByTestId } = render(<AssistantSettingsButton {...testProps} />);
|
||||
fireEvent.click(getByTestId('settings'));
|
||||
expect(setSelectedSettingsTab).toHaveBeenCalledWith(CONVERSATIONS_TAB);
|
||||
expect(setIsSettingsModalVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('Settings modal is visible and calls correct actions per click', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantSettingsButton {...testProps} isSettingsModalVisible />
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
|
||||
import { DataStreamApis } from '../use_data_stream_apis';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
|
@ -13,6 +14,7 @@ import { Conversation } from '../../..';
|
|||
import { AssistantSettings } from './assistant_settings';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { CONVERSATIONS_TAB } from './const';
|
||||
|
||||
interface Props {
|
||||
defaultConnector?: AIConnector;
|
||||
|
@ -45,7 +47,11 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
refetchCurrentUserConversations,
|
||||
refetchPrompts,
|
||||
}) => {
|
||||
const { toasts } = useAssistantContext();
|
||||
const {
|
||||
assistantFeatures: { assistantKnowledgeBaseByDefault },
|
||||
toasts,
|
||||
setSelectedSettingsTab,
|
||||
} = useAssistantContext();
|
||||
|
||||
// Modal control functions
|
||||
const cleanupAndCloseModal = useCallback(() => {
|
||||
|
@ -73,18 +79,39 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
[cleanupAndCloseModal, refetchCurrentUserConversations, refetchPrompts, toasts]
|
||||
);
|
||||
|
||||
const handleShowConversationSettings = useCallback(() => {
|
||||
setSelectedSettingsTab(CONVERSATIONS_TAB);
|
||||
setIsSettingsModalVisible(true);
|
||||
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
|
||||
|
||||
return (
|
||||
isSettingsModalVisible && (
|
||||
<AssistantSettings
|
||||
defaultConnector={defaultConnector}
|
||||
selectedConversationId={selectedConversationId}
|
||||
onConversationSelected={onConversationSelected}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSave}
|
||||
conversations={conversations}
|
||||
conversationsLoaded={conversationsLoaded}
|
||||
/>
|
||||
)
|
||||
<>
|
||||
{!assistantKnowledgeBaseByDefault && (
|
||||
<EuiToolTip position="right" content={i18n.SETTINGS_TOOLTIP}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.SETTINGS}
|
||||
data-test-subj="settings"
|
||||
onClick={handleShowConversationSettings}
|
||||
isDisabled={isDisabled}
|
||||
iconType="gear"
|
||||
size="xs"
|
||||
color="text"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
|
||||
{isSettingsModalVisible && (
|
||||
<AssistantSettings
|
||||
defaultConnector={defaultConnector}
|
||||
selectedConversationId={selectedConversationId}
|
||||
onConversationSelected={onConversationSelected}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSave}
|
||||
conversations={conversations}
|
||||
conversationsLoaded={conversationsLoaded}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -42,10 +42,10 @@ describe('IndexEntryEditor', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the form fields with initial values', () => {
|
||||
it('renders the form fields with initial values', async () => {
|
||||
const { getByDisplayValue } = render(<IndexEntryEditor {...defaultProps} />);
|
||||
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(getByDisplayValue('Test Entry')).toBeInTheDocument();
|
||||
expect(getByDisplayValue('Test Description')).toBeInTheDocument();
|
||||
expect(getByDisplayValue('Test Query Description')).toBeInTheDocument();
|
||||
|
@ -54,35 +54,37 @@ describe('IndexEntryEditor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('updates the name field on change', () => {
|
||||
it('updates the name field on change', async () => {
|
||||
const { getByTestId } = render(<IndexEntryEditor {...defaultProps} />);
|
||||
|
||||
waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const nameInput = getByTestId('entry-name');
|
||||
fireEvent.change(nameInput, { target: { value: 'New Entry Name' } });
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('updates the description field on change', () => {
|
||||
it('updates the description field on change', async () => {
|
||||
const { getByTestId } = render(<IndexEntryEditor {...defaultProps} />);
|
||||
waitFor(() => {
|
||||
|
||||
await waitFor(() => {
|
||||
const descriptionInput = getByTestId('entry-description');
|
||||
fireEvent.change(descriptionInput, { target: { value: 'New Description' } });
|
||||
});
|
||||
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
await waitFor(() => {
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the query description field on change', () => {
|
||||
it('updates the query description field on change', async () => {
|
||||
const { getByTestId } = render(<IndexEntryEditor {...defaultProps} />);
|
||||
waitFor(() => {
|
||||
|
||||
await waitFor(() => {
|
||||
const queryDescriptionInput = getByTestId('query-description');
|
||||
fireEvent.change(queryDescriptionInput, { target: { value: 'New Query Description' } });
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('displays sharing options and updates on selection', async () => {
|
||||
|
@ -91,8 +93,6 @@ describe('IndexEntryEditor', () => {
|
|||
await waitFor(() => {
|
||||
fireEvent.click(getByTestId('sharing-select'));
|
||||
fireEvent.click(getByTestId('sharing-private-option'));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
@ -100,28 +100,25 @@ describe('IndexEntryEditor', () => {
|
|||
it('fetches index options and updates on selection', async () => {
|
||||
const { getAllByTestId, getByTestId } = render(<IndexEntryEditor {...defaultProps} />);
|
||||
|
||||
await waitFor(() => expect(mockDataViews.getIndices).toHaveBeenCalled());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDataViews.getIndices).toHaveBeenCalled();
|
||||
fireEvent.click(getByTestId('index-combobox'));
|
||||
fireEvent.click(getAllByTestId('comboBoxToggleListButton')[0]);
|
||||
fireEvent.click(getByTestId('index-2'));
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
fireEvent.click(getByTestId('index-2'));
|
||||
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('fetches field options based on selected index and updates on selection', async () => {
|
||||
const { getByTestId, getAllByTestId } = render(<IndexEntryEditor {...defaultProps} />);
|
||||
|
||||
await waitFor(() =>
|
||||
await waitFor(() => {
|
||||
expect(mockDataViews.getFieldsForWildcard).toHaveBeenCalledWith({
|
||||
pattern: 'index-1',
|
||||
fieldTypes: ['semantic_text'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
fireEvent.click(getByTestId('index-combobox'));
|
||||
fireEvent.click(getAllByTestId('comboBoxToggleListButton')[0]);
|
||||
});
|
||||
|
@ -135,7 +132,10 @@ describe('IndexEntryEditor', () => {
|
|||
within(getByTestId('entry-combobox')).getByTestId('comboBoxSearchInput'),
|
||||
'field-3'
|
||||
);
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetEntry).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the field combo box if no index is selected', () => {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
EuiSuperSelect,
|
||||
} from '@elastic/eui';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { IndexEntry } from '@kbn/elastic-assistant-common';
|
||||
import { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
|
@ -96,29 +96,37 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
|
|||
}));
|
||||
}, [dataViews]);
|
||||
|
||||
const fieldOptions = useAsync(async () => {
|
||||
const fields = await dataViews.getFieldsForWildcard({
|
||||
pattern: entry?.index ?? '',
|
||||
fieldTypes: ['semantic_text'],
|
||||
});
|
||||
const indexFields = useAsync(
|
||||
async () =>
|
||||
dataViews.getFieldsForWildcard({
|
||||
pattern: entry?.index ?? '',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return fields
|
||||
.filter((field) => field.esTypes?.includes('semantic_text'))
|
||||
.map((field) => ({
|
||||
const fieldOptions = useMemo(
|
||||
() =>
|
||||
indexFields?.value
|
||||
?.filter((field) => field.esTypes?.includes('semantic_text'))
|
||||
.map((field) => ({
|
||||
'data-test-subj': field.name,
|
||||
label: field.name,
|
||||
value: field.name,
|
||||
})) ?? [],
|
||||
[indexFields?.value]
|
||||
);
|
||||
|
||||
const outputFieldOptions = useMemo(
|
||||
() =>
|
||||
indexFields?.value?.map((field) => ({
|
||||
'data-test-subj': field.name,
|
||||
label: field.name,
|
||||
value: field.name,
|
||||
}));
|
||||
}, [entry]);
|
||||
|
||||
const setIndex = useCallback(
|
||||
async (e: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setEntry((prevEntry) => ({ ...prevEntry, index: e[0]?.value }));
|
||||
},
|
||||
[setEntry]
|
||||
})) ?? [],
|
||||
[indexFields?.value]
|
||||
);
|
||||
|
||||
const onCreateOption = (searchValue: string) => {
|
||||
const onCreateIndexOption = (searchValue: string) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
|
@ -131,7 +139,6 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
|
|||
};
|
||||
|
||||
setIndex([newOption]);
|
||||
setField([{ label: '', value: '' }]);
|
||||
};
|
||||
|
||||
const onCreateFieldOption = (searchValue: string) => {
|
||||
|
@ -170,6 +177,52 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
|
|||
[setEntry]
|
||||
);
|
||||
|
||||
// Field
|
||||
const setOutputFields = useCallback(
|
||||
async (e: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setEntry((prevEntry) => ({
|
||||
...prevEntry,
|
||||
outputFields: e
|
||||
?.filter((option) => !!option.value)
|
||||
.map((option) => option.value as string),
|
||||
}));
|
||||
},
|
||||
[setEntry]
|
||||
);
|
||||
|
||||
const setIndex = useCallback(
|
||||
async (e: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
setEntry((prevEntry) => ({ ...prevEntry, index: e[0]?.value }));
|
||||
setField([]);
|
||||
setOutputFields([]);
|
||||
},
|
||||
[setEntry, setField, setOutputFields]
|
||||
);
|
||||
|
||||
const onCreateOutputFieldsOption = useCallback(
|
||||
(searchValue: string) => {
|
||||
const normalizedSearchValue = searchValue.trim().toLowerCase();
|
||||
|
||||
if (!normalizedSearchValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOption: EuiComboBoxOptionOption<string> = {
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
};
|
||||
|
||||
setOutputFields([
|
||||
...(entry?.outputFields?.map((field) => ({
|
||||
label: field,
|
||||
value: field,
|
||||
})) ?? []),
|
||||
newOption,
|
||||
]);
|
||||
},
|
||||
[entry?.outputFields, setOutputFields]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
|
@ -204,7 +257,7 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
|
|||
aria-label={i18n.ENTRY_INDEX_NAME_INPUT_LABEL}
|
||||
isClearable={true}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onCreateOption={onCreateOption}
|
||||
onCreateOption={onCreateIndexOption}
|
||||
fullWidth
|
||||
options={indexOptions.value ?? []}
|
||||
selectedOptions={
|
||||
|
@ -228,7 +281,7 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
|
|||
singleSelection={{ asPlainText: true }}
|
||||
onCreateOption={onCreateFieldOption}
|
||||
fullWidth
|
||||
options={fieldOptions.value ?? []}
|
||||
options={fieldOptions}
|
||||
selectedOptions={
|
||||
entry?.field
|
||||
? [
|
||||
|
@ -281,11 +334,17 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
|
|||
<EuiComboBox
|
||||
aria-label={i18n.ENTRY_OUTPUT_FIELDS_INPUT_LABEL}
|
||||
isClearable={true}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onCreateOption={onCreateOption}
|
||||
onCreateOption={onCreateOutputFieldsOption}
|
||||
fullWidth
|
||||
selectedOptions={[]}
|
||||
onChange={setIndex}
|
||||
options={outputFieldOptions}
|
||||
isDisabled={!entry?.index?.length}
|
||||
selectedOptions={
|
||||
entry?.outputFields?.map((field) => ({
|
||||
label: field,
|
||||
value: field,
|
||||
})) ?? []
|
||||
}
|
||||
onChange={setOutputFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
|
|
|
@ -26,6 +26,9 @@ export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
|||
app: [ASSISTANT_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
minimumLicense: 'enterprise',
|
||||
management: {
|
||||
kibana: ['securityAiAssistantManagement'],
|
||||
},
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['elasticAssistant'],
|
||||
|
@ -36,6 +39,9 @@ export const getAssistantBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
|||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
management: {
|
||||
kibana: ['securityAiAssistantManagement'],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
// No read-only mode currently supported
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue