[8.16] [Security Assistant] Fix KB output fields (#196567) (#197118)

# 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![image](https://github.com/user-attachments/assets/2460cf22-c02a-4513-98d1-5fbcd75d117b)","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![image](https://github.com/user-attachments/assets/2460cf22-c02a-4513-98d1-5fbcd75d117b)","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![image](https://github.com/user-attachments/assets/2460cf22-c02a-4513-98d1-5fbcd75d117b)","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:
Kibana Machine 2024-10-22 07:19:44 +11:00 committed by GitHub
parent 52483614f5
commit 18b517a9fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 406 additions and 65 deletions

View file

@ -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);

View file

@ -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,

View file

@ -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

View file

@ -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 />

View file

@ -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}
/>
)}
</>
);
}
);

View file

@ -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', () => {

View file

@ -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>

View file

@ -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