Add Security AI assistant settings to the Stack management (#176656)

## Summary

<img width="3005" alt="Zrzut ekranu 2024-04-2 o 22 58 37"
src="f7814891-d018-45e6-96a2-3da3321d56fd">

<img width="3006" alt="Zrzut ekranu 2024-04-2 o 22 58 45"
src="a1ec8d96-b48e-4f57-9a6c-3f1823d164f1">

<img width="3007" alt="Zrzut ekranu 2024-04-2 o 22 58 54"
src="f67fc0f0-b28c-40c8-8b25-5a180c115610">

<img width="3005" alt="Zrzut ekranu 2024-04-2 o 23 38 32"
src="e79631ea-c87c-4dd1-8fe6-c5d257cf2fe7">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Garrett Spong <spong@users.noreply.github.com>
Co-authored-by: Garrett Spong <garrett.spong@elastic.co>
Co-authored-by: Tomasz Ciecierski <tomasz.ciecierski@elastic.co>
This commit is contained in:
Patryk Kopyciński 2024-04-16 21:15:11 +02:00 committed by GitHub
parent 4546d274e8
commit b53624d472
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 612 additions and 84 deletions

View file

@ -28,6 +28,7 @@ export type IntegrationsDeepLinkId = IntegrationsAppId | FleetAppId | OsQueryApp
export type ManagementAppId = typeof MANAGEMENT_APP_ID;
export type ManagementId =
| 'aiAssistantManagementSelection'
| 'securityAiAssistantManagement'
| 'observabilityAiAssistantManagement'
| 'api_keys'
| 'cases'

View file

@ -131,7 +131,7 @@ pageLoadAssetSize:
searchPlayground: 19325
searchprofiler: 67080
security: 81771
securitySolution: 82780
securitySolution: 98429
securitySolutionEss: 16573
securitySolutionServerless: 62488
serverless: 16573

View file

@ -25,6 +25,7 @@ export function AiAssistantSelectionPage() {
const { capabilities, setBreadcrumbs, navigateToApp } = useAppContext();
const observabilityAIAssistantEnabled = capabilities.observabilityAIAssistant.show;
const securityAIAssistantEnabled = capabilities.securitySolutionAssistant?.['ai-assistant'];
useEffect(() => {
setBreadcrumbs([
@ -113,6 +114,55 @@ export function AiAssistantSelectionPage() {
}
/>
</EuiFlexItem>
<EuiFlexItem grow>
<EuiCard
description={
<div>
{!securityAIAssistantEnabled ? (
<>
<EuiSpacer size="s" />
<EuiCallOut
iconType="warning"
title={i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.thisFeatureIsDisabledCallOutLabel',
{
defaultMessage:
'This feature is disabled. It can be enabled from Spaces > Features.',
}
)}
size="s"
/>
<EuiSpacer size="s" />
</>
) : null}
<EuiLink
data-test-subj="securityAiAssistantSelectionPageDocumentationLink"
external
target="_blank"
href="https://www.elastic.co/guide/en/security/current/security-assistant.html"
>
{i18n.translate(
'aiAssistantManagementSelection.aiAssistantSettingsPage.securityAssistant.documentationLinkLabel',
{ defaultMessage: 'Documentation' }
)}
</EuiLink>
</div>
}
display="plain"
hasBorder
icon={<EuiIcon size="l" type="logoSecurity" />}
isDisabled={!securityAIAssistantEnabled}
layout="horizontal"
title={i18n.translate(
'aiAssistantManagementSelection.aiAssistantSelectionPage.securityLabel',
{ defaultMessage: 'Elastic AI Assistant for Security' }
)}
titleSize="xs"
onClick={() =>
navigateToApp('management', { path: 'kibana/securityAiAssistantManagement' })
}
/>
</EuiFlexItem>
</EuiFlexGrid>
</>
);

View file

@ -17,6 +17,7 @@ export const capabilitiesProvider = () => ({
indexPatterns: true,
objects: true,
aiAssistantManagementSelection: true,
securityAiAssistantManagement: true,
observabilityAiAssistantManagement: true,
},
},

View file

@ -0,0 +1,339 @@
/*
* 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, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiIcon,
EuiFlexItem,
EuiPageTemplate,
EuiFlexGroup,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { Conversation, Prompt, QuickPrompt } from '../../..';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { useSettingsUpdater } from './use_settings_updater/use_settings_updater';
import {
AnonymizationSettings,
ConversationSettings,
EvaluationSettings,
KnowledgeBaseSettings,
QuickPromptSettings,
SystemPromptSettings,
} from '.';
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
import { getDefaultConnector } from '../helpers';
import { useFetchAnonymizationFields } from '../api/anonymization_fields/use_fetch_anonymization_fields';
export const CONVERSATIONS_TAB = 'CONVERSATION_TAB' as const;
export const QUICK_PROMPTS_TAB = 'QUICK_PROMPTS_TAB' as const;
export const SYSTEM_PROMPTS_TAB = 'SYSTEM_PROMPTS_TAB' as const;
export const ANONYMIZATION_TAB = 'ANONYMIZATION_TAB' as const;
export const KNOWLEDGE_BASE_TAB = 'KNOWLEDGE_BASE_TAB' as const;
export const EVALUATION_TAB = 'EVALUATION_TAB' as const;
export type SettingsTabs =
| typeof CONVERSATIONS_TAB
| typeof QUICK_PROMPTS_TAB
| typeof SYSTEM_PROMPTS_TAB
| typeof ANONYMIZATION_TAB
| typeof KNOWLEDGE_BASE_TAB
| typeof EVALUATION_TAB;
interface Props {
conversations: Record<string, Conversation>;
selectedConversation: Conversation;
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
isFlyoutMode: boolean;
}
/**
* Modal for overall Assistant Settings, including conversation settings, quick prompts, system prompts,
* anonymization, knowledge base, and evaluation via the `isModelEvaluationEnabled` feature flag.
*/
export const AssistantSettingsManagement: React.FC<Props> = React.memo(
({
selectedConversation: defaultSelectedConversation,
setSelectedConversationId,
conversations,
isFlyoutMode,
}) => {
const {
actionTypeRegistry,
modelEvaluatorEnabled,
http,
selectedSettingsTab,
setSelectedSettingsTab,
toasts,
} = useAssistantContext();
const { data: anonymizationFields } = useFetchAnonymizationFields();
// Connector details
const { data: connectors } = useLoadConnectors({
http,
});
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const {
conversationSettings,
setConversationSettings,
knowledgeBase,
quickPromptSettings,
systemPromptSettings,
assistantStreamingEnabled,
setUpdatedAssistantStreamingEnabled,
setUpdatedKnowledgeBaseSettings,
setUpdatedQuickPromptSettings,
setUpdatedSystemPromptSettings,
saveSettings,
conversationsSettingsBulkActions,
updatedAnonymizationData,
setConversationsSettingsBulkActions,
anonymizationFieldsBulkActions,
setAnonymizationFieldsBulkActions,
setUpdatedAnonymizationData,
resetSettings,
} = useSettingsUpdater(
conversations,
anonymizationFields ?? { page: 0, perPage: 0, total: 0, data: [] }
);
// Local state for saving previously selected items so tab switching is friendlier
// Conversation Selection State
const [selectedConversation, setSelectedConversation] = useState<Conversation | undefined>(
() => {
return conversationSettings[defaultSelectedConversation.title];
}
);
const onHandleSelectedConversationChange = useCallback((conversation?: Conversation) => {
setSelectedConversation(conversation);
}, []);
useEffect(() => {
if (selectedConversation != null) {
setSelectedConversation(conversationSettings[selectedConversation.title]);
}
}, [conversationSettings, selectedConversation]);
// Quick Prompt Selection State
const [selectedQuickPrompt, setSelectedQuickPrompt] = useState<QuickPrompt | undefined>();
const onHandleSelectedQuickPromptChange = useCallback((quickPrompt?: QuickPrompt) => {
setSelectedQuickPrompt(quickPrompt);
}, []);
useEffect(() => {
if (selectedQuickPrompt != null) {
setSelectedQuickPrompt(
quickPromptSettings.find((q) => q.title === selectedQuickPrompt.title)
);
}
}, [quickPromptSettings, selectedQuickPrompt]);
// System Prompt Selection State
const [selectedSystemPrompt, setSelectedSystemPrompt] = useState<Prompt | undefined>();
const onHandleSelectedSystemPromptChange = useCallback((systemPrompt?: Prompt) => {
setSelectedSystemPrompt(systemPrompt);
}, []);
useEffect(() => {
if (selectedSystemPrompt != null) {
setSelectedSystemPrompt(systemPromptSettings.find((p) => p.id === selectedSystemPrompt.id));
}
}, [selectedSystemPrompt, systemPromptSettings]);
const handleSave = useCallback(() => {
// If the selected conversation is deleted, we need to select a new conversation to prevent a crash creating a conversation that already exists
const isSelectedConversationDeleted =
conversationSettings[defaultSelectedConversation.title] == null;
const newSelectedConversationId: string | undefined = Object.keys(conversationSettings)[0];
if (isSelectedConversationDeleted && newSelectedConversationId != null) {
setSelectedConversationId(conversationSettings[newSelectedConversationId].title);
}
saveSettings();
toasts?.addSuccess({
iconType: 'check',
title: i18n.SETTINGS_UPDATED_TOAST_TITLE,
});
}, [
conversationSettings,
defaultSelectedConversation.title,
saveSettings,
setSelectedConversationId,
toasts,
]);
const tabsConfig = useMemo(
() => [
{
id: CONVERSATIONS_TAB,
label: i18n.CONVERSATIONS_MENU_ITEM,
prepend: <EuiIcon type="discuss" />,
},
{
id: QUICK_PROMPTS_TAB,
label: i18n.QUICK_PROMPTS_MENU_ITEM,
prepend: <EuiIcon type="editorComment" />,
},
{
id: SYSTEM_PROMPTS_TAB,
label: i18n.SYSTEM_PROMPTS_MENU_ITEM,
prepend: <EuiIcon type="editorComment" />,
},
{
id: ANONYMIZATION_TAB,
label: i18n.ANONYMIZATION_MENU_ITEM,
prepend: <EuiIcon type="eyeClosed" />,
},
{
id: KNOWLEDGE_BASE_TAB,
label: i18n.KNOWLEDGE_BASE_MENU_ITEM,
prepend: <EuiIcon type="notebookApp" />,
},
...(modelEvaluatorEnabled
? [
{
id: EVALUATION_TAB,
label: i18n.EVALUATION_MENU_ITEM,
prepend: <EuiIcon type="crossClusterReplicationApp" />,
},
]
: []),
],
[modelEvaluatorEnabled]
);
const tabs = useMemo(() => {
return tabsConfig.map((t) => ({
...t,
'data-test-subj': `settingsPageTab-${t.id}`,
onClick: () => setSelectedSettingsTab(t.id),
isSelected: t.id === selectedSettingsTab,
}));
}, [setSelectedSettingsTab, selectedSettingsTab, tabsConfig]);
const handleChange = useCallback(
(callback) => (value: unknown) => {
setHasPendingChanges(true);
callback(value);
},
[]
);
const onCancelClick = useCallback(() => {
resetSettings();
setHasPendingChanges(false);
}, [resetSettings]);
return (
<>
<EuiPageTemplate.Header pageTitle="Settings" tabs={tabs} paddingSize="none" />
<EuiPageTemplate.Section
paddingSize="l"
css={css`
padding-left: 0;
padding-right: 0;
`}
>
{selectedSettingsTab === CONVERSATIONS_TAB && (
<ConversationSettings
actionTypeRegistry={actionTypeRegistry}
defaultConnector={defaultConnector}
conversationSettings={conversationSettings}
setConversationsSettingsBulkActions={handleChange(
setConversationsSettingsBulkActions
)}
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
setConversationSettings={handleChange(setConversationSettings)}
allSystemPrompts={systemPromptSettings}
selectedConversation={selectedConversation}
isDisabled={selectedConversation == null}
assistantStreamingEnabled={assistantStreamingEnabled}
setAssistantStreamingEnabled={handleChange(setUpdatedAssistantStreamingEnabled)}
onSelectedConversationChange={onHandleSelectedConversationChange}
http={http}
isFlyoutMode={isFlyoutMode}
/>
)}
{selectedSettingsTab === QUICK_PROMPTS_TAB && (
<QuickPromptSettings
quickPromptSettings={quickPromptSettings}
onSelectedQuickPromptChange={onHandleSelectedQuickPromptChange}
selectedQuickPrompt={selectedQuickPrompt}
setUpdatedQuickPromptSettings={handleChange(setUpdatedQuickPromptSettings)}
/>
)}
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
<SystemPromptSettings
conversationSettings={conversationSettings}
defaultConnector={defaultConnector}
systemPromptSettings={systemPromptSettings}
onSelectedSystemPromptChange={onHandleSelectedSystemPromptChange}
selectedSystemPrompt={selectedSystemPrompt}
setConversationSettings={handleChange(setConversationSettings)}
setConversationsSettingsBulkActions={handleChange(
setConversationsSettingsBulkActions
)}
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
setUpdatedSystemPromptSettings={handleChange(setUpdatedSystemPromptSettings)}
/>
)}
{selectedSettingsTab === ANONYMIZATION_TAB && (
<AnonymizationSettings
defaultPageSize={5}
anonymizationFields={updatedAnonymizationData}
anonymizationFieldsBulkActions={anonymizationFieldsBulkActions}
setAnonymizationFieldsBulkActions={handleChange(setAnonymizationFieldsBulkActions)}
setUpdatedAnonymizationData={handleChange(setUpdatedAnonymizationData)}
/>
)}
{selectedSettingsTab === KNOWLEDGE_BASE_TAB && (
<KnowledgeBaseSettings
knowledgeBase={knowledgeBase}
setUpdatedKnowledgeBaseSettings={handleChange(setUpdatedKnowledgeBaseSettings)}
/>
)}
{selectedSettingsTab === EVALUATION_TAB && <EvaluationSettings />}
</EuiPageTemplate.Section>
{hasPendingChanges && (
<EuiPageTemplate.BottomBar paddingSize="s" position="fixed">
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
color="text"
iconType="cross"
data-test-subj="cancel-button"
onClick={onCancelClick}
>
{i18n.CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
type="submit"
data-test-subj="save-button"
onClick={handleSave}
iconType="check"
fill
>
{i18n.SAVE}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageTemplate.BottomBar>
)}
</>
);
}
);
AssistantSettingsManagement.displayName = 'AssistantSettingsNew';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { FindAnonymizationFieldsResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.gen';
import { PerformBulkActionRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
import { Conversation, Prompt, QuickPrompt } from '../../../..';
@ -172,6 +172,22 @@ export const useSettingsUpdater = (
assistantTelemetry,
]);
useEffect(() => {
if (
!(
anonymizationFieldsBulkActions.create?.length ||
anonymizationFieldsBulkActions.update?.length ||
anonymizationFieldsBulkActions.delete?.ids?.length
)
)
setUpdatedAnonymizationData(anonymizationFields);
}, [
anonymizationFields,
anonymizationFieldsBulkActions.create?.length,
anonymizationFieldsBulkActions.delete?.ids?.length,
anonymizationFieldsBulkActions.update?.length,
]);
return {
conversationSettings,
conversationsSettingsBulkActions,

View file

@ -50,14 +50,8 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
const {
data: aiConnectors,
isLoading: isLoadingConnectors,
isFetching: isFetchingConnectors,
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const { data: aiConnectors, refetch: refetchConnectors } = useLoadConnectors({ http });
const isLoading = isLoadingConnectors || isFetchingConnectors;
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
const addNewConnectorOption = useMemo(() => {
@ -97,7 +91,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
return {
value: connector.id,
'data-test-subj': connector.id,
inputDisplay: displayFancy ? displayFancy(connector.name) : connector.name,
inputDisplay: displayFancy?.(connector.name) ?? connector.name,
dropdownDisplay: (
<React.Fragment key={connector.id}>
<strong>{connector.name}</strong>
@ -165,7 +159,6 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
data-test-subj="connector-selector"
disabled={localIsDisabled}
hasDividers={true}
isLoading={isLoading}
isOpen={modalForceOpen}
onChange={onChange}
options={allConnectorOptions}
@ -178,7 +171,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
<AddConnectorModal
actionTypeRegistry={actionTypeRegistry}
actionTypes={actionTypes}
onClose={cleanupAndCloseModal}
onClose={() => setIsConnectorModalVisible(false)}
onSaveConnector={onSaveConnector}
onSelectActionType={(actionType: ActionType) => setSelectedActionType(actionType)}
selectedActionType={selectedActionType}

View file

@ -27,7 +27,6 @@ interface Props {
isFlyoutMode: boolean;
onConnectorIdSelected?: (connectorId: string) => void;
onConnectorSelected?: (conversation: Conversation) => void;
showLabel?: boolean;
}
const inputContainerClassName = css`
@ -57,9 +56,7 @@ const placeholderButtonClassName = css`
text-overflow: ellipsis;
max-width: 400px;
font-weight: normal;
padding-bottom: 5px;
padding-left: 0;
padding-top: 2px;
padding: 0 14px 0 0;
`;
/**
@ -71,7 +68,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
selectedConnectorId,
selectedConversation,
isFlyoutMode,
showLabel = true,
onConnectorIdSelected,
onConnectorSelected,
}) => {
@ -139,13 +136,6 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
justifyContent={'flexStart'}
responsive={false}
>
{!isFlyoutMode && (
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{i18n.INLINE_CONNECTOR_LABEL}
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem>
<ConnectorSelector
displayFancy={(displayText) => (
@ -177,18 +167,11 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
justifyContent={'flexStart'}
responsive={false}
>
{showLabel && (
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{i18n.INLINE_CONNECTOR_LABEL}
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem>
{isOpen ? (
<ConnectorSelector
displayFancy={(displayText) => (
<EuiText css={inputDisplayClassName} size="s">
<EuiText className={inputDisplayClassName} size="xs">
{displayText}
</EuiText>
)}

View file

@ -64,6 +64,7 @@
"usageCollection",
"lists",
"home",
"management",
"telemetry",
"dataViewFieldEditor",
"osquery",

View file

@ -47,7 +47,6 @@ const HeaderComponent: React.FC<Props> = ({
onConnectorSelected={noop}
onConnectorIdSelected={onConnectorIdSelected}
selectedConnectorId={connectorId}
showLabel={false}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -12,7 +12,7 @@ import type { Store, Action } from 'redux';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { AppLeaveHandler, AppMountParameters } from '@kbn/core/public';
import type { AppMountParameters } from '@kbn/core/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { CellActionsProvider } from '@kbn/cell-actions';
@ -36,18 +36,11 @@ import { AssistantProvider } from '../assistant/provider';
interface StartAppComponent {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
store: Store<State, Action>;
theme$: AppMountParameters['theme$'];
}
const StartAppComponent: FC<StartAppComponent> = ({
children,
history,
onAppLeave,
store,
theme$,
}) => {
const StartAppComponent: FC<StartAppComponent> = ({ children, history, store, theme$ }) => {
const services = useKibana().services;
const {
i18n,
@ -78,9 +71,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
<UpsellingProvider upsellingService={upselling}>
<DiscoverInTimelineContextProvider>
<AssistantProvider>
<PageRouter history={history} onAppLeave={onAppLeave}>
{children}
</PageRouter>
<PageRouter history={history}>{children}</PageRouter>
</AssistantProvider>
</DiscoverInTimelineContextProvider>
</UpsellingProvider>
@ -107,7 +98,6 @@ const StartApp = memo(StartAppComponent);
interface SecurityAppComponentProps {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
services: StartServices;
store: Store<State, Action>;
theme$: AppMountParameters['theme$'];
@ -116,7 +106,6 @@ interface SecurityAppComponentProps {
const SecurityAppComponent: React.FC<SecurityAppComponentProps> = ({
children,
history,
onAppLeave,
services,
store,
theme$,
@ -131,7 +120,7 @@ const SecurityAppComponent: React.FC<SecurityAppComponentProps> = ({
}}
>
<CloudProvider>
<StartApp history={history} onAppLeave={onAppLeave} store={store} theme$={theme$}>
<StartApp history={history} store={store} theme$={theme$}>
{children}
</StartApp>
</CloudProvider>

View file

@ -53,22 +53,24 @@ export const GlobalHeader = React.memo(() => {
const { href, onClick } = useAddIntegrationsUrl();
useEffect(() => {
setHeaderActionMenu((element) => {
const mount = toMountPoint(<OutPortal node={portalNode} />, {
theme,
i18n: kibanaServiceI18n,
if (setHeaderActionMenu) {
setHeaderActionMenu((element) => {
const mount = toMountPoint(<OutPortal node={portalNode} />, {
theme,
i18n: kibanaServiceI18n,
});
return mount(element);
});
return mount(element);
});
return () => {
/* Dashboard mounts an edit toolbar, it should be restored when leaving dashboard editing page */
if (dashboardViewPath) {
return;
}
portalNode.unmount();
setHeaderActionMenu(undefined);
};
return () => {
/* Dashboard mounts an edit toolbar, it should be restored when leaving dashboard editing page */
if (dashboardViewPath) {
return;
}
portalNode.unmount();
setHeaderActionMenu(undefined);
};
}
}, [portalNode, setHeaderActionMenu, theme, kibanaServiceI18n, dashboardViewPath]);
return (

View file

@ -6,6 +6,7 @@
*/
import React from 'react';
import { noop } from 'lodash/fp';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineWrapper } from '../../../../timelines/wrapper';
@ -19,7 +20,9 @@ export const Timeline = React.memo(() => {
const { onAppLeave } = useKibana().services;
return <TimelineWrapper timelineId={TimelineId.active} onAppLeave={onAppLeave} />;
return (
<TimelineWrapper timelineId={TimelineId.active} onAppLeave={onAppLeave ? onAppLeave : noop} />
);
});
Timeline.displayName = 'Timeline';

View file

@ -14,25 +14,20 @@ import { AppRoutes } from './app_routes';
export const renderApp = ({
element,
history,
onAppLeave,
services,
store,
usageCollection,
subPluginRoutes,
theme$,
children,
}: RenderAppProps): (() => void) => {
const ApplicationUsageTrackingProvider =
usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
render(
<SecurityApp
history={history}
onAppLeave={onAppLeave}
services={services}
store={store}
theme$={theme$}
>
<SecurityApp history={history} services={services} store={store} theme$={theme$}>
<ApplicationUsageTrackingProvider>
<AppRoutes subPluginRoutes={subPluginRoutes} services={services} />
{children ??
(subPluginRoutes && <AppRoutes subPluginRoutes={subPluginRoutes} services={services} />)}
</ApplicationUsageTrackingProvider>
</SecurityApp>,
element

View file

@ -10,7 +10,6 @@ import type { FC } from 'react';
import React, { memo, useEffect } from 'react';
import { Router, Routes, Route } from '@kbn/shared-ux-router';
import { useDispatch } from 'react-redux';
import type { AppLeaveHandler } from '@kbn/core/public';
import { APP_ID } from '../../common/constants';
import { RouteCapture } from '../common/components/endpoint/route_capture';
@ -23,10 +22,9 @@ import { HomePage } from './home';
interface RouterProps {
children: React.ReactNode;
history: History;
onAppLeave: (handler: AppLeaveHandler) => void;
}
const PageRouterComponent: FC<RouterProps> = ({ children, history, onAppLeave }) => {
const PageRouterComponent: FC<RouterProps> = ({ children, history }) => {
const { cases } = useKibana().services;
const CasesContext = cases.ui.getCasesContext();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);

View file

@ -25,11 +25,13 @@ import type { StartServices } from '../types';
/**
* The React properties used to render `SecurityApp` as well as the `element` to render it into.
*/
export interface RenderAppProps extends AppMountParameters {
export interface RenderAppProps
extends Omit<AppMountParameters, 'appBasePath' | 'onAppLeave' | 'setHeaderActionMenu'> {
services: StartServices;
store: Store<State, Action>;
subPluginRoutes: RouteProps[];
subPluginRoutes?: RouteProps[];
usageCollection?: UsageCollectionSetup;
children?: React.ReactNode;
}
import type { State, SubPluginsInitReducer } from '../common/store';

View file

@ -0,0 +1,68 @@
/*
* 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, useMemo, useState } from 'react';
import { AssistantSettingsManagement } from '@kbn/elastic-assistant/impl/assistant/settings/assistant_settings_management';
import type { Conversation } from '@kbn/elastic-assistant';
import {
mergeBaseWithPersistedConversations,
useAssistantContext,
useFetchCurrentUserConversations,
WELCOME_CONVERSATION_TITLE,
} from '@kbn/elastic-assistant';
import { useConversation } from '@kbn/elastic-assistant/impl/assistant/use_conversation';
import type { FetchConversationsResponse } from '@kbn/elastic-assistant/impl/assistant/api';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
export const ManagementSettings = React.memo(() => {
const isFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode');
const {
baseConversations,
http,
assistantAvailability: { isAssistantEnabled },
} = useAssistantContext();
const onFetchedConversations = useCallback(
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
[baseConversations]
);
const { data: conversations } = useFetchCurrentUserConversations({
http,
onFetch: onFetchedConversations,
isAssistantEnabled,
});
const [selectedConversationId, setSelectedConversationId] = useState<string>(
WELCOME_CONVERSATION_TITLE
);
const { getDefaultConversation } = useConversation();
const currentConversation = useMemo(
() =>
conversations?.[selectedConversationId] ??
getDefaultConversation({ cTitle: WELCOME_CONVERSATION_TITLE, isFlyoutMode }),
[conversations, getDefaultConversation, selectedConversationId, isFlyoutMode]
);
if (conversations) {
return (
<AssistantSettingsManagement
selectedConversation={currentConversation}
setSelectedConversationId={setSelectedConversationId}
conversations={conversations}
isFlyoutMode={isFlyoutMode}
/>
);
}
return <></>;
});
ManagementSettings.displayName = 'ManagementSettings';

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
/**
* the plugin (defined in `plugin.tsx`) has many dependencies that can be loaded only when the app is being used.
* By loading these later we can reduce the initial bundle size and allow users to delay loading these dependencies until they are needed.
*/
import { ManagementSettings } from './assistant/stack_management/management_settings';
export { ManagementSettings };

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { Subject, combineLatestWith } from 'rxjs';
import type * as H from 'history';
@ -93,7 +94,14 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
): PluginSetup {
this.services.setup(core, plugins);
const { home, triggersActionsUi, usageCollection } = plugins;
const { home, triggersActionsUi, usageCollection, management } = plugins;
const assistantManagementTitle = i18n.translate(
'xpack.securitySolution.securityAiAssistantManagement.app.title',
{
defaultMessage: 'AI Assistant for Security',
}
);
if (home) {
home.featureCatalogue.registerSolution({
@ -107,6 +115,54 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
path: APP_PATH,
order: 300,
});
home.featureCatalogue.register({
id: 'ai_assistant_security',
title: assistantManagementTitle,
description: i18n.translate(
'xpack.securitySolution.securityAiAssistantManagement.app.description',
{
defaultMessage: 'Manage your AI Assistant for Security.',
}
),
icon: 'sparkles',
path: '/app/management/kibana/securityAiAssistantManagement',
showOnHomePage: false,
category: 'admin',
});
if (management) {
management.sections.section.kibana.registerApp({
id: 'securityAiAssistantManagement',
title: assistantManagementTitle,
hideFromSidebar: true,
order: 1,
mount: async (params) => {
// required to show the alert table inside cases
const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi;
const { registerAlertsTableConfiguration } =
await this.lazyRegisterAlertsTableConfiguration();
registerAlertsTableConfiguration(alertsTableConfigurationRegistry, this.storage);
const [coreStart, startPlugins] = await core.getStartServices();
const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins);
const store = await this.store(coreStart, startPlugins, subPlugins);
const services = await this.services.generateServices(coreStart, startPlugins);
await this.registerActions(store, params.history, services);
const { renderApp } = await this.lazyApplicationDependencies();
const { ManagementSettings } = await this.lazyAssistantSettingsManagement();
return renderApp({
...params,
services,
store,
usageCollection: plugins.usageCollection,
children: <ManagementSettings />,
});
},
});
}
}
const mount: AppMount = async (params) => {
@ -468,4 +524,15 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
'./actions'
);
}
private lazyAssistantSettingsManagement() {
/**
* The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues.
* See https://webpack.js.org/api/module-methods/#magic-comments
*/
return import(
/* webpackChunkName: "actions" */
'./lazy_assistant_settings_management'
);
}
}

View file

@ -98,7 +98,7 @@ export class PluginServices {
public async generateServices(
coreStart: CoreStart,
startPlugins: StartPluginsDependencies,
params: AppMountParameters<unknown>
params?: AppMountParameters<unknown>
): Promise<StartServices> {
const { apm } = await import('@elastic/apm-rum');
const { SecuritySolutionTemplateWrapper } = await import('./app/home/template_wrapper');
@ -119,11 +119,11 @@ export class PluginServices {
apm,
configSettings: this.configSettings,
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
setHeaderActionMenu: params.setHeaderActionMenu,
...(params?.setHeaderActionMenu ? { setHeaderActionMenu: params.setHeaderActionMenu } : {}),
storage: this.storage,
sessionStorage: this.sessionStorage,
security: startPlugins.security,
onAppLeave: params.onAppLeave,
...(params?.onAppLeave ? { onAppLeave: params.onAppLeave } : {}),
securityLayout: { getPluginWrapper: () => SecuritySolutionTemplateWrapper },
contentManagement: startPlugins.contentManagement,
telemetry: this.telemetry.start(),

View file

@ -52,6 +52,7 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { ManagementSetup } from '@kbn/management-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
@ -93,6 +94,7 @@ export interface SetupPlugins {
cloud?: CloudSetup;
home?: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
management: ManagementSetup;
security: SecurityPluginSetup;
triggersActionsUi: TriggersActionsSetup;
usageCollection?: UsageCollectionSetup;
@ -167,8 +169,8 @@ export type StartServices = CoreStart &
sessionStorage: Storage;
apm: ApmBase;
savedObjectsTagging?: SavedObjectsTaggingApi;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
onAppLeave: (handler: AppLeaveHandler) => void;
setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu'];
onAppLeave?: (handler: AppLeaveHandler) => void;
/**
* This component will be exposed to all lazy loaded plugins, via useKibana hook. It should wrap every plugin route.

View file

@ -561,6 +561,9 @@ export class Plugin implements ISecuritySolutionPlugin {
assistantAlertsInsights: config.experimentalFeatures.assistantAlertsInsights,
assistantModelEvaluation: config.experimentalFeatures.assistantModelEvaluation,
});
plugins.elasticAssistant.registerFeatures('management', {
assistantModelEvaluation: config.experimentalFeatures.assistantModelEvaluation,
});
if (this.lists && plugins.taskManager && plugins.fleet) {
// Exceptions, Artifacts and Manifests start

View file

@ -197,6 +197,7 @@
"@kbn/data-service",
"@kbn/core-chrome-browser",
"@kbn/shared-ux-chrome-navigation",
"@kbn/core-ui-settings-browser-mocks"
"@kbn/core-ui-settings-browser-mocks",
"@kbn/management-plugin"
]
}